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.dialog.SettingsLayoutActivity;
33import com.android.tv.settings.dialog.Layout;
34import com.android.tv.settings.dialog.Layout.Header;
35import com.android.tv.settings.dialog.Layout.Action;
36import com.android.tv.settings.dialog.Layout.Status;
37import com.android.tv.settings.dialog.Layout.Static;
38import com.android.tv.settings.dialog.Layout.StringGetter;
39import com.android.tv.settings.dialog.Layout.LayoutGetter;
40
41import com.android.tv.settings.R;
42
43import java.util.ArrayList;
44import java.util.Collections;
45import java.util.Comparator;
46import java.util.HashMap;
47import java.util.LinkedHashMap;
48import java.util.Map;
49import java.util.Set;
50
51/**
52 * Activity to control TV input settings.
53 */
54public 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
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
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
178    private static Bundle createData(TvInputInfo info) {
179        Bundle data = new Bundle();
180        data.putString(KEY_ID, info.getId());
181        return data;
182    }
183
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
190    private static int getOnOffResId(boolean enabled) {
191        return enabled ? R.string.on : R.string.off;
192    }
193
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