1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.settings.system;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.res.Resources;
22 import android.media.tv.TvInputInfo;
23 import android.media.tv.TvInputInfo.TvInputSettings;
24 import android.media.tv.TvInputManager;
25 import android.os.Bundle;
26 import android.os.UserHandle;
27 import android.provider.Settings.Global;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.util.Pair;
31 
32 import com.android.tv.settings.dialog.SettingsLayoutActivity;
33 import com.android.tv.settings.dialog.Layout;
34 import com.android.tv.settings.dialog.Layout.Header;
35 import com.android.tv.settings.dialog.Layout.Action;
36 import com.android.tv.settings.dialog.Layout.Status;
37 import com.android.tv.settings.dialog.Layout.Static;
38 import com.android.tv.settings.dialog.Layout.StringGetter;
39 import com.android.tv.settings.dialog.Layout.LayoutGetter;
40 
41 import com.android.tv.settings.R;
42 
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.Comparator;
46 import java.util.HashMap;
47 import java.util.LinkedHashMap;
48 import java.util.Map;
49 import java.util.Set;
50 
51 /**
52  * Activity to control TV input settings.
53  */
54 public class InputsActivity extends SettingsLayoutActivity {
55     private static final String TAG = "InputsActivity";
56     private static final boolean DEBUG = false;
57 
58     private static final int ACTION_EDIT_LABEL = 0;
59     private static final int ACTION_CUSTOM_LABEL = 1;
60     private static final int ACTION_HIDE = 2;
61     private static final int ACTION_HDMI_CONTROL = 3;
62     private static final int ACTION_DEVICE_AUTO_OFF = 4;
63     private static final int ACTION_TV_AUTO_ON = 5;
64 
65     private static final String KEY_ID = "id";
66     private static final String KEY_LABEL = "label";
67     private static final String KEY_ON = "on";
68 
69     private static final int REQUEST_CODE_CUSTOM_LABEL = 0;
70 
71     private static final int DISABLED = 0;
72     private static final int ENABLED = 1;
73 
74     private static final int PREDEFINED_LABEL_RES_IDS[] = {
75         R.string.inputs_blu_ray,
76         R.string.inputs_cable,
77         R.string.inputs_dvd,
78         R.string.inputs_game,
79         R.string.inputs_aux
80     };
81 
82     private static final LinkedHashMap<Integer, Integer> STATE_STRING_ID_MAP =
83             new LinkedHashMap<Integer, Integer>() {{
84                 put(TvInputManager.INPUT_STATE_CONNECTED,
85                         R.plurals.inputs_header_connected_input);
86                 put(TvInputManager.INPUT_STATE_CONNECTED_STANDBY,
87                         R.plurals.inputs_header_standby_input);
88                 put(TvInputManager.INPUT_STATE_DISCONNECTED,
89                         R.plurals.inputs_header_disconnected_input);
90             }};
91 
92     private TvInputManager mTvInputManager;
93     private Resources mRes;
94     private Map<String, String> mCustomLabels;
95     private Set<String> mHiddenIds;
96 
97     @Override
onCreate(Bundle savedInstanceState)98     public void onCreate(Bundle savedInstanceState) {
99         mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
100         mRes = getResources();
101         mCustomLabels = TvInputSettings.getCustomLabels(this, UserHandle.USER_OWNER);
102         mHiddenIds = TvInputSettings.getHiddenTvInputIds(this, UserHandle.USER_OWNER);
103         super.onCreate(savedInstanceState);
104     }
105 
106     LayoutGetter mInputListLayout = new LayoutGetter() {
107         @Override
108         public Layout get() {
109             return getExternalTvInputListLayout();
110         }
111     };
112 
113     LayoutGetter mCecSettingsLayout = new LayoutGetter() {
114         @Override
115         public Layout get() {
116             boolean hdmiControl = readCecOption(getCecOptionKey(ACTION_HDMI_CONTROL));
117             boolean deviceAutoOff = readCecOption(getCecOptionKey(ACTION_DEVICE_AUTO_OFF));
118             boolean tvAutoOn = readCecOption(getCecOptionKey(ACTION_TV_AUTO_ON));
119 
120             return
121                 new Layout()
122                     .add(new Header.Builder(mRes)
123                             .title(R.string.inputs_hdmi_control)
124                             .description(getOnOffResId(hdmiControl))
125                             .detailedDescription(R.string.inputs_hdmi_control_desc)
126                             .build()
127                         .add(getOnOffLayout(
128                                 R.string.inputs_hdmi_control,
129                                 R.string.inputs_hdmi_control_desc,
130                                 ACTION_HDMI_CONTROL,
131                                 null,
132                                 hdmiControl)))
133                     .add(new Header.Builder(mRes)
134                             .title(R.string.inputs_device_auto_off)
135                             .description(getOnOffResId(deviceAutoOff))
136                             .detailedDescription(R.string.inputs_device_auto_off_desc)
137                             .build()
138                         .add(getOnOffLayout(
139                                 R.string.inputs_device_auto_off,
140                                 R.string.inputs_device_auto_off_desc,
141                                 ACTION_DEVICE_AUTO_OFF,
142                                 null,
143                                 deviceAutoOff)))
144                     .add(new Header.Builder(mRes)
145                             .title(R.string.inputs_tv_auto_on)
146                             .description(getOnOffResId(tvAutoOn))
147                             .detailedDescription(R.string.inputs_tv_auto_on_desc)
148                             .build()
149                         .add(getOnOffLayout(
150                                 R.string.inputs_tv_auto_on,
151                                 R.string.inputs_tv_auto_on_desc,
152                                 ACTION_TV_AUTO_ON,
153                                 null,
154                                 tvAutoOn)));
155         }
156     };
157 
158     @Override
createLayout()159     public Layout createLayout() {
160         return
161             new Layout()
162                 .breadcrumb(getString(R.string.header_category_preferences))
163                 .add(new Header.Builder(mRes)
164                         .icon(R.drawable.ic_settings_inputs)
165                         .title(R.string.inputs_inputs)
166                         .build()
167                     .add(mInputListLayout)
168                     .add(new Static.Builder(mRes)
169                             .title(R.string.inputs_header_cec)
170                             .build())
171                     .add(new Header.Builder(mRes)
172                              .title(R.string.inputs_cec_settings)
173                              .build()
174                         .add(mCecSettingsLayout))
175                     );
176     }
177 
createData(TvInputInfo info)178     private static Bundle createData(TvInputInfo info) {
179         Bundle data = new Bundle();
180         data.putString(KEY_ID, info.getId());
181         return data;
182     }
183 
createData(TvInputInfo info, String label)184     private static Bundle createData(TvInputInfo info, String label) {
185         Bundle data = createData(info);
186         data.putString(KEY_LABEL, label);
187         return data;
188     }
189 
getOnOffResId(boolean enabled)190     private static int getOnOffResId(boolean enabled) {
191         return enabled ? R.string.on : R.string.off;
192     }
193 
getOnOffLayout(final int titleResId, final int descResId, final int action, final Bundle data, final boolean checked)194     private LayoutGetter getOnOffLayout(final int titleResId, final int descResId,
195             final int action, final Bundle data, final boolean checked) {
196         return new LayoutGetter() {
197             @Override
198             public Layout get() {
199                 Bundle on = (data == null) ? new Bundle() : new Bundle(data);
200                 on.putBoolean(KEY_ON, true);
201                 Bundle off = (data == null) ? new Bundle() : new Bundle(data);
202                 off.putBoolean(KEY_ON, false);
203 
204                 Layout layout = new Layout()
205                             .add(new Action.Builder(mRes, action)
206                                     .title(R.string.on)
207                                     .data(on)
208                                     .checked(checked == true)
209                                     .build())
210                             .add(new Action.Builder(mRes, action)
211                                     .title(R.string.off)
212                                     .data(off)
213                                     .checked(checked == false)
214                                     .build());
215 
216                 return layout;
217             }
218         };
219     }
220 
221     private LayoutGetter getEditLabelLayout(final TvInputInfo info) {
222         return new LayoutGetter() {
223             @Override
224             public Layout get() {
225                 String defaultLabel = info.loadLabel(InputsActivity.this).toString();
226                 String customLabel = mCustomLabels.get(info.getId());
227                 boolean isHidden = mHiddenIds.contains(info.getId());
228                 boolean isChecked = false;
229 
230                 // Add default.
231                 boolean isDefault = !isHidden && TextUtils.isEmpty(customLabel);
232                 Layout layout = new Layout()
233                     .add(new Action.Builder(mRes, ACTION_EDIT_LABEL)
234                         .title(defaultLabel)
235                         .data(createData(info, null))
236                         .checked(isDefault)
237                         .build());
238                 isChecked |= isDefault;
239 
240                 // Add pre-defined labels.
241                 for (int i = 0; i < PREDEFINED_LABEL_RES_IDS.length; i++) {
242                     String name = getString(PREDEFINED_LABEL_RES_IDS[i]);
243                     boolean checked = !isHidden && name.equals(customLabel);
244                     layout.add(new Action.Builder(mRes, ACTION_EDIT_LABEL)
245                               .title(name)
246                               .data(createData(info, name))
247                               .checked(checked)
248                               .build());
249                     isChecked |= checked;
250                 }
251 
252                 // Add hidden.
253                 layout.add(new Action.Builder(mRes, ACTION_HIDE)
254                           .title(R.string.inputs_hide)
255                           .description(R.string.inputs_hide_desc)
256                           .data(createData(info))
257                           .checked(isHidden)
258                           .build());
259                 isChecked |= isHidden;
260 
261                 // Add custom Label.
262                 String label =  (isChecked) ? defaultLabel : customLabel;
263                 layout.add(new Action.Builder(mRes, ACTION_CUSTOM_LABEL)
264                           .title(R.string.inputs_custom_name)
265                           .data(createData(info, label))
266                           .description(label)
267                           .checked(!isChecked)
268                           .build());
269 
270                 return layout;
271             }
272         };
273     }
274 
275     private Layout getExternalTvInputListLayout() {
276         HashMap<Integer, ArrayList<Pair<String, TvInputInfo>>> externalInputs =
277                 new HashMap<Integer, ArrayList<Pair<String, TvInputInfo>>>();
278         for (TvInputInfo info : mTvInputManager.getTvInputList()) {
279             if (info.getType() != TvInputInfo.TYPE_TUNER &&
280                     TextUtils.isEmpty(info.getParentId())) {
281                 int state;
282                 try {
283                     state = mTvInputManager.getInputState(info.getId());
284                 } catch (IllegalArgumentException e) {
285                     // Input is gone while iterating. Ignore.
286                     continue;
287                 }
288 
289                 ArrayList list = externalInputs.get(state);
290                 if (list == null) {
291                     list = new ArrayList<Pair<String, TvInputInfo>>();
292                     externalInputs.put(state, list);
293                 }
294                 // Cache label because loadLabel does binder call internally
295                 // and it would be the sort key.
296                 list.add(Pair.create(info.loadLabel(this).toString(), info));
297             }
298         }
299 
300         for (ArrayList<Pair<String, TvInputInfo>> list : externalInputs.values()) {
301             Collections.sort(list, new Comparator<Pair<String, TvInputInfo>>() {
302                 @Override
303                 public int compare(Pair<String, TvInputInfo> a, Pair<String, TvInputInfo> b) {
304                     return a.first.compareTo(b.first);
305                 }
306             });
307         }
308 
309         Layout layout = new Layout();
310         for (LinkedHashMap.Entry<Integer, Integer> state : STATE_STRING_ID_MAP.entrySet()) {
311             ArrayList<Pair<String, TvInputInfo>> list = externalInputs.get(state.getKey());
312             if (list != null && list.size() > 0) {
313                 String header = mRes.getQuantityString(state.getValue(), list.size());
314                 layout.add(new Static.Builder(mRes)
315                           .title(header)
316                           .build());
317                 for (Pair<String, TvInputInfo> input : list) {
318                     String customLabel;
319                     if (mHiddenIds.contains(input.second.getId())) {
320                         customLabel = getString(R.string.inputs_hide);
321                     } else {
322                         customLabel = mCustomLabels.get(input.second.getId());
323                         if (TextUtils.isEmpty(customLabel)) {
324                             customLabel = input.second.loadLabel(this).toString();
325                         }
326                     }
327                     layout.add(new Header.Builder(mRes)
328                                   .title(input.first)
329                                   .description(customLabel)
330                                   .build()
331                               .add(getEditLabelLayout(input.second)));
332                 }
333             }
334         }
335 
336         return layout;
337     }
338 
339     @Override
340     public void onActionClicked(Action action) {
341         switch (action.getId()) {
342             case ACTION_EDIT_LABEL:
343                 handleEditLabel(action);
344                 goBackToTitle(getString(R.string.inputs_inputs));
345                 break;
346             case ACTION_CUSTOM_LABEL:
347                 displayCustomLabelActivity(action.getData());
348                 goBackToTitle(getString(R.string.inputs_inputs));
349                 break;
350             case ACTION_HIDE:
351                 handleHide(action);
352                 goBackToTitle(getString(R.string.inputs_inputs));
353                 break;
354             case ACTION_HDMI_CONTROL:
355             case ACTION_DEVICE_AUTO_OFF:
356             case ACTION_TV_AUTO_ON:
357                 handleCecOption(action);
358                 goBackToTitle(getString(R.string.inputs_cec_settings));
359                 break;
360         }
361     }
362 
363     private void handleEditLabel(Action action) {
364         String id = action.getData().getString(KEY_ID);
365         String name = action.getData().getString(KEY_LABEL);
366         saveCustomLabel(id, name);
367     }
368 
369     private void handleHide(Action action) {
370         handleHide(action.getData().getString(KEY_ID), true);
371     }
372 
373     private void handleHide(String inputId, boolean hide) {
374         if (DEBUG) Log.d(TAG, "Hide " + inputId + ": " + hide);
375 
376         boolean changed = false;
377         if (hide) {
378             if (!mHiddenIds.contains(inputId)) {
379                 mHiddenIds.add(inputId);
380                 changed = true;
381             }
382         } else {
383             if (mHiddenIds.contains(inputId)) {
384                 mHiddenIds.remove(inputId);
385                 changed = true;
386             }
387         }
388         if (changed) {
389             TvInputSettings.putHiddenTvInputs(this, mHiddenIds, UserHandle.USER_OWNER);
390         }
391     }
392 
393     private void handleCecOption(Action action) {
394         String key = getCecOptionKey(action.getId());
395         boolean enabled = action.getData().getBoolean(KEY_ON);
396         writeCecOption(key, enabled);
397     }
398 
399     private void saveCustomLabel(String inputId, String label) {
400         if (DEBUG) Log.d(TAG, "Setting " + inputId + " => " + label);
401 
402         if (!TextUtils.isEmpty(label)) {
403             mCustomLabels.put(inputId, label);
404         } else {
405             mCustomLabels.remove(inputId);
406         }
407 
408         TvInputSettings.putCustomLabels(this, mCustomLabels, UserHandle.USER_OWNER);
409         handleHide(inputId, false);
410     }
411 
412     private void displayCustomLabelActivity(Bundle data) {
413         String id = data.getString(KEY_ID);
414         Intent intent = new Intent(this, InputsCustomLabelActivity.class);
415         intent.putExtra(InputsCustomLabelActivity.KEY_ID, data.getString(KEY_ID));
416         intent.putExtra(InputsCustomLabelActivity.KEY_LABEL,
417                 data.getString(KEY_LABEL));
418         startActivityForResult(intent, REQUEST_CODE_CUSTOM_LABEL);
419     }
420 
421     @Override
422     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
423         if (requestCode == REQUEST_CODE_CUSTOM_LABEL) {
424             if (resultCode == InputsCustomLabelActivity.RESULT_OK) {
425                 String inputId = data.getStringExtra(InputsCustomLabelActivity.KEY_ID);
426                 String label = data.getStringExtra(InputsCustomLabelActivity.KEY_LABEL);
427                 saveCustomLabel(inputId, label);
428                 goBackToTitle(getString(R.string.inputs_inputs));
429             }
430         }
431     }
432 
433     private String getCecOptionKey(int action) {
434         switch (action) {
435             case ACTION_HDMI_CONTROL:
436                 return Global.HDMI_CONTROL_ENABLED;
437             case ACTION_DEVICE_AUTO_OFF:
438                 return Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED;
439             case ACTION_TV_AUTO_ON:
440                 return Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED;
441         }
442         return "";
443     }
444 
445     private static int toInt(boolean enabled) {
446         return enabled ? ENABLED : DISABLED;
447     }
448 
449     private boolean readCecOption(String key) {
450         return Global.getInt(getContentResolver(), key, toInt(true)) == ENABLED;
451     }
452 
453     private void writeCecOption(String key, boolean value) {
454         if (DEBUG) {
455             Log.d(TAG, "Writing CEC option " + key + " to " + value);
456         }
457         Global.putInt(getContentResolver(), key, toInt(value));
458     }
459 }
460