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