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;
18
19import android.accounts.Account;
20import android.accounts.AccountManager;
21import android.bluetooth.BluetoothAdapter;
22import android.bluetooth.BluetoothDevice;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.res.TypedArray;
27import android.content.res.XmlResourceParser;
28import android.media.tv.TvInputInfo;
29import android.media.tv.TvInputManager;
30import android.os.Bundle;
31import android.os.Handler;
32import android.preference.PreferenceActivity;
33import android.support.v17.leanback.widget.ArrayObjectAdapter;
34import android.support.v17.leanback.widget.HeaderItem;
35import android.support.v17.leanback.widget.ObjectAdapter;
36import android.support.v17.leanback.widget.ListRow;
37import android.util.AttributeSet;
38import android.util.Log;
39import android.util.TypedValue;
40import android.util.Xml;
41
42import com.android.internal.util.XmlUtils;
43
44import com.android.tv.settings.accessories.AccessoryUtils;
45import com.android.tv.settings.accessories.BluetoothAccessoryActivity;
46import com.android.tv.settings.accounts.AccountImageUriGetter;
47import com.android.tv.settings.accounts.AccountSettingsActivity;
48import com.android.tv.settings.accounts.AuthenticatorHelper;
49import com.android.tv.settings.connectivity.ConnectivityStatusIconUriGetter;
50import com.android.tv.settings.connectivity.ConnectivityStatusTextGetter;
51import com.android.tv.settings.connectivity.WifiNetworksActivity;
52import com.android.tv.settings.device.sound.SoundActivity;
53import com.android.tv.settings.users.RestrictedProfileActivity;
54import com.android.tv.settings.util.UriUtils;
55
56import org.xmlpull.v1.XmlPullParser;
57import org.xmlpull.v1.XmlPullParserException;
58
59import java.io.IOException;
60import java.util.HashSet;
61import java.util.Set;
62
63/**
64 * Gets the list of browse headers and browse items.
65 */
66public class BrowseInfo extends BrowseInfoBase {
67
68    private static final String TAG = "CanvasSettings.BrowseInfo";
69    private static final boolean DEBUG = false;
70
71    public static final String EXTRA_ACCESSORY_ADDRESS = "accessory_address";
72    public static final String EXTRA_ACCESSORY_NAME = "accessory_name";
73    public static final String EXTRA_ACCESSORY_ICON_ID = "accessory_icon_res";
74
75    private static final String ACCOUNT_TYPE_GOOGLE = "com.google";
76
77    private static final String ETHERNET_PREFERENCE_KEY = "ethernet";
78
79    interface XmlReaderListener {
80        void handleRequestedNode(Context context, XmlResourceParser parser, AttributeSet attrs)
81                throws org.xmlpull.v1.XmlPullParserException, IOException;
82    }
83
84    static class SoundActivityImageUriGetter implements MenuItem.UriGetter {
85
86        private final Context mContext;
87
88        SoundActivityImageUriGetter(Context context) {
89            mContext = context;
90        }
91
92        @Override
93        public String getUri() {
94            return UriUtils.getAndroidResourceUri(mContext.getResources(),
95                    SoundActivity.getIconResource(mContext.getContentResolver()));
96        }
97    }
98
99    static class XmlReader {
100
101        private final Context mContext;
102        private final int mXmlResource;
103        private final String mRootNodeName;
104        private final String mNodeNameRequested;
105        private final XmlReaderListener mListener;
106
107        XmlReader(Context context, int xmlResource, String rootNodeName, String nodeNameRequested,
108                XmlReaderListener listener) {
109            mContext = context;
110            mXmlResource = xmlResource;
111            mRootNodeName = rootNodeName;
112            mNodeNameRequested = nodeNameRequested;
113            mListener = listener;
114        }
115
116        void read() {
117            XmlResourceParser parser = null;
118            try {
119                parser = mContext.getResources().getXml(mXmlResource);
120                AttributeSet attrs = Xml.asAttributeSet(parser);
121
122                int type;
123                while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
124                        && type != XmlPullParser.START_TAG) {
125                    // Parse next until start tag is found
126                }
127
128                String nodeName = parser.getName();
129                if (!mRootNodeName.equals(nodeName)) {
130                    throw new RuntimeException("XML document must start with <" + mRootNodeName
131                            + "> tag; found" + nodeName + " at " + parser.getPositionDescription());
132                }
133
134                Bundle curBundle = null;
135
136                final int outerDepth = parser.getDepth();
137                while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
138                        && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
139                    if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
140                        continue;
141                    }
142
143                    nodeName = parser.getName();
144                    if (mNodeNameRequested.equals(nodeName)) {
145                        mListener.handleRequestedNode(mContext, parser, attrs);
146                    } else {
147                        XmlUtils.skipCurrentTag(parser);
148                    }
149                }
150
151            } catch (XmlPullParserException e) {
152                throw new RuntimeException("Error parsing headers", e);
153            } catch (IOException e) {
154                throw new RuntimeException("Error parsing headers", e);
155            } finally {
156                if (parser != null)
157                    parser.close();
158            }
159        }
160    }
161
162    private static final String PREF_KEY_ADD_ACCOUNT = "add_account";
163    private static final String PREF_KEY_ADD_ACCESSORY = "add_accessory";
164    private static final String PREF_KEY_WIFI = "wifi";
165    private static final String PREF_KEY_DEVELOPER = "developer";
166    private static final String PREF_KEY_INPUTS = "inputs";
167
168    private final Context mContext;
169    private final AuthenticatorHelper mAuthenticatorHelper;
170    private int mNextItemId;
171    private int mAccountHeaderId;
172    private final BluetoothAdapter mBtAdapter;
173    private final Set<BluetoothDevice> mConnectedDevices;
174    private final Object mGuard = new Object();
175    private final boolean mAllowMultipleAccounts;
176    private MenuItem mWifiItem = null;
177    private ArrayObjectAdapter mWifiRow = null;
178    private final Handler mHandler = new Handler();
179
180    private PreferenceUtils mPreferenceUtils;
181    private boolean mDeveloperEnabled;
182    private boolean mInputSettingNeeded;
183
184    private final Runnable refreshWifiCardRunnable = new Runnable() {
185        public void run() {
186            refreshWifiCard();
187        }
188    };
189
190    BrowseInfo(Context context) {
191        mContext = context;
192        mAuthenticatorHelper = new AuthenticatorHelper();
193        mAuthenticatorHelper.updateAuthDescriptions(context);
194        mAuthenticatorHelper.onAccountsUpdated(context, null);
195        mBtAdapter = BluetoothAdapter.getDefaultAdapter();
196        mConnectedDevices = new HashSet<BluetoothDevice>();
197        mNextItemId = 0;
198        mAllowMultipleAccounts =
199                context.getResources().getBoolean(R.bool.multiple_accounts_enabled);
200        mPreferenceUtils = new PreferenceUtils(context);
201        mDeveloperEnabled = mPreferenceUtils.isDeveloperEnabled();
202        mInputSettingNeeded = isInputSettingNeeded();
203    }
204
205    @Override
206    public void refreshContent() {
207        init();
208    }
209
210    void init() {
211        synchronized (mGuard) {
212            mHeaderItems.clear();
213            mRows.clear();
214            int settingsXml = isRestricted() ? R.xml.restricted_main : R.xml.main;
215            new XmlReader(mContext, settingsXml, "preference-headers", "header",
216                    new HeaderXmlReaderListener()).read();
217            updateAccessories(R.id.accessories);
218        }
219    }
220
221    void checkForDeveloperOptionUpdate() {
222        final boolean developerEnabled = mPreferenceUtils.isDeveloperEnabled();
223        if (developerEnabled != mDeveloperEnabled) {
224            mDeveloperEnabled = developerEnabled;
225            init();
226        }
227    }
228
229    private class HeaderXmlReaderListener implements XmlReaderListener {
230        @Override
231        public void handleRequestedNode(Context context, XmlResourceParser parser,
232                AttributeSet attrs)
233                throws XmlPullParserException, IOException {
234            TypedArray sa = mContext.getResources().obtainAttributes(attrs,
235                    com.android.internal.R.styleable.PreferenceHeader);
236            final int headerId = sa.getResourceId(
237                    com.android.internal.R.styleable.PreferenceHeader_id,
238                    (int) PreferenceActivity.HEADER_ID_UNDEFINED);
239            String title = getStringFromTypedArray(sa,
240                    com.android.internal.R.styleable.PreferenceHeader_title);
241            sa.recycle();
242            sa = context.getResources().obtainAttributes(attrs, R.styleable.CanvasSettings);
243            int preferenceRes = sa.getResourceId(R.styleable.CanvasSettings_preference, 0);
244            sa.recycle();
245            mHeaderItems.add(new HeaderItem(headerId, title, null));
246            final ArrayObjectAdapter currentRow = new ArrayObjectAdapter();
247            mRows.put(headerId, currentRow);
248            if (headerId != R.id.accessories) {
249                new XmlReader(context, preferenceRes, "PreferenceScreen", "Preference",
250                        new PreferenceXmlReaderListener(headerId, currentRow)).read();
251            }
252        }
253    }
254
255    private boolean canAddAccount() {
256        return !isRestricted();
257    }
258
259    private boolean isRestricted() {
260        return RestrictedProfileActivity.isRestrictedProfileInEffect(mContext);
261    }
262
263    private class PreferenceXmlReaderListener implements XmlReaderListener {
264
265        private final int mHeaderId;
266        private final ArrayObjectAdapter mRow;
267
268        PreferenceXmlReaderListener(int headerId, ArrayObjectAdapter row) {
269            mHeaderId = headerId;
270            mRow = row;
271        }
272
273        @Override
274        public void handleRequestedNode(Context context, XmlResourceParser parser,
275                AttributeSet attrs) throws XmlPullParserException, IOException {
276            TypedArray sa = context.getResources().obtainAttributes(attrs,
277                    com.android.internal.R.styleable.Preference);
278
279            String key = getStringFromTypedArray(sa,
280                    com.android.internal.R.styleable.Preference_key);
281            String title = getStringFromTypedArray(sa,
282                    com.android.internal.R.styleable.Preference_title);
283            int iconRes = sa.getResourceId(com.android.internal.R.styleable.Preference_icon,
284                    R.drawable.settings_default_icon);
285            sa.recycle();
286
287            if (PREF_KEY_ADD_ACCOUNT.equals(key)) {
288                mAccountHeaderId = mHeaderId;
289                addAccounts(mRow);
290            } else if ((!key.equals(PREF_KEY_DEVELOPER) || mDeveloperEnabled)
291                    && (!key.equals(PREF_KEY_INPUTS) || mInputSettingNeeded)) {
292                MenuItem.TextGetter descriptionGetter = getDescriptionTextGetterFromKey(key);
293                MenuItem.UriGetter uriGetter = getIconUriGetterFromKey(key);
294                MenuItem.Builder builder = new MenuItem.Builder().id(mNextItemId++).title(title)
295                        .descriptionGetter(descriptionGetter)
296                        .intent(getIntent(parser, attrs, mHeaderId));
297                if(uriGetter == null) {
298                    builder.imageResourceId(mContext, iconRes);
299                } else {
300                    builder.imageUriGetter(uriGetter);
301                }
302                if (key.equals(PREF_KEY_WIFI)) {
303                    mWifiItem = builder.build();
304                    mRow.add(mWifiItem);
305                    mWifiRow = mRow;
306                } else {
307                    mRow.add(builder.build());
308                }
309            }
310        }
311    }
312
313    private void refreshWifiCard() {
314        if (mWifiItem != null) {
315            int index = mWifiRow.indexOf(mWifiItem);
316            if (index >= 0) {
317                mWifiRow.notifyArrayItemRangeChanged(index, 1);
318            }
319        }
320    }
321
322    void rebuildInfo() {
323        init();
324    }
325
326    void updateAccounts() {
327        synchronized (mGuard) {
328            if (isRestricted()) {
329                // We don't display the accounts in restricted mode
330                return;
331            }
332            ArrayObjectAdapter row = mRows.get(mAccountHeaderId);
333            // Clear any account row cards that are not "Location" or "Security".
334            String dontDelete[] = new String[2];
335            dontDelete[0] = mContext.getString(R.string.system_location);
336            dontDelete[1] = mContext.getString(R.string.system_security);
337            int i = 0;
338            while (i < row.size ()) {
339                MenuItem menuItem = (MenuItem) row.get(i);
340                String title = menuItem.getTitle ();
341                boolean deleteItem = true;
342                for (int j = 0; j < dontDelete.length; ++j) {
343                    if (title.equals(dontDelete[j])) {
344                        deleteItem = false;
345                        break;
346                    }
347                }
348                if (deleteItem) {
349                    row.removeItems(i, 1);
350                } else {
351                    ++i;
352                }
353            }
354            // Add accounts to end of row.
355            addAccounts(row);
356        }
357    }
358
359    void updateAccessories() {
360        synchronized (mGuard) {
361            updateAccessories(R.id.accessories);
362        }
363    }
364
365    public void updateWifi() {
366        mHandler.post(refreshWifiCardRunnable);
367    }
368
369    void bluetoothDeviceConnected(BluetoothDevice device) {
370        synchronized (mConnectedDevices) {
371            mConnectedDevices.add(device);
372        }
373    }
374
375    void bluetoothDeviceDisconnected(BluetoothDevice device) {
376        synchronized (mConnectedDevices) {
377            mConnectedDevices.remove(device);
378        }
379    }
380
381    boolean isDeviceConnected(BluetoothDevice device) {
382        synchronized (mConnectedDevices) {
383            return mConnectedDevices.contains(device);
384        }
385    }
386
387    private boolean isInputSettingNeeded() {
388        TvInputManager manager = (TvInputManager) mContext.getSystemService(
389                Context.TV_INPUT_SERVICE);
390        if (manager != null) {
391            for (TvInputInfo input : manager.getTvInputList()) {
392                if (input.isPassthroughInput()) {
393                    return true;
394                }
395            }
396        }
397        return false;
398    }
399
400    private void updateAccessories(int headerId) {
401        ArrayObjectAdapter row = mRows.get(headerId);
402        row.clear();
403
404        addAccessories(row);
405
406        // Add new accessory activity icon
407        ComponentName componentName = new ComponentName("com.android.tv.settings",
408                "com.android.tv.settings.accessories.AddAccessoryActivity");
409        Intent i = new Intent().setComponent(componentName);
410        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
411        row.add(new MenuItem.Builder().id(mNextItemId++)
412                .title(mContext.getString(R.string.accessories_add))
413                .imageResourceId(mContext, R.drawable.ic_settings_bluetooth)
414                .intent(i).build());
415    }
416
417    private Intent getIntent(XmlResourceParser parser, AttributeSet attrs, int headerId)
418            throws org.xmlpull.v1.XmlPullParserException, IOException {
419        Intent intent = null;
420        if (parser.next() == XmlPullParser.START_TAG && "intent".equals(parser.getName())) {
421            TypedArray sa = mContext.getResources()
422                    .obtainAttributes(attrs, com.android.internal.R.styleable.Intent);
423            String targetClass = getStringFromTypedArray(
424                    sa, com.android.internal.R.styleable.Intent_targetClass);
425            String targetPackage = getStringFromTypedArray(
426                    sa, com.android.internal.R.styleable.Intent_targetPackage);
427            String action = getStringFromTypedArray(
428                    sa, com.android.internal.R.styleable.Intent_action);
429            if (targetClass != null && targetPackage != null) {
430                ComponentName componentName = new ComponentName(targetPackage, targetClass);
431                intent = new Intent();
432                intent.setComponent(componentName);
433            } else if (action != null) {
434                intent = new Intent(action);
435            }
436
437            XmlUtils.skipCurrentTag(parser);
438        }
439        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
440        return intent;
441    }
442
443    private String getStringFromTypedArray(TypedArray sa, int resourceId) {
444        String value = null;
445        TypedValue tv = sa.peekValue(resourceId);
446        if (tv != null && tv.type == TypedValue.TYPE_STRING) {
447            if (tv.resourceId != 0) {
448                value = mContext.getString(tv.resourceId);
449            } else {
450                value = tv.string.toString();
451            }
452        }
453        return value;
454    }
455
456    private MenuItem.TextGetter getDescriptionTextGetterFromKey(String key) {
457        if (WifiNetworksActivity.PREFERENCE_KEY.equals(key)) {
458            return ConnectivityStatusTextGetter.createWifiStatusTextGetter(mContext);
459        }
460
461        if (ETHERNET_PREFERENCE_KEY.equals(key)) {
462            return ConnectivityStatusTextGetter.createEthernetStatusTextGetter(mContext);
463        }
464
465        return null;
466    }
467
468    private MenuItem.UriGetter getIconUriGetterFromKey(String key) {
469        if (SoundActivity.getPreferenceKey().equals(key)) {
470            return new SoundActivityImageUriGetter(mContext);
471        }
472
473        if (WifiNetworksActivity.PREFERENCE_KEY.equals(key)) {
474            return ConnectivityStatusIconUriGetter.createWifiStatusIconUriGetter(mContext);
475        }
476
477        return null;
478    }
479
480    private void addAccounts(ArrayObjectAdapter row) {
481        String[] accountTypes = mAuthenticatorHelper.getEnabledAccountTypes();
482        if (accountTypes.length == 0) {
483            // That's weird, let's try updating.
484            mAuthenticatorHelper.onAccountsUpdated(mContext, null);
485            accountTypes = mAuthenticatorHelper.getEnabledAccountTypes();
486        }
487
488        int googleAccountCount = 0;
489
490        for (String accountType : accountTypes) {
491            CharSequence label = mAuthenticatorHelper.getLabelForType(mContext, accountType);
492            if (label == null) {
493                continue;
494            }
495
496            Account[] accounts = AccountManager.get(mContext).getAccountsByType(accountType);
497            if (ACCOUNT_TYPE_GOOGLE.equals(accountType)) {
498                googleAccountCount = accounts.length;
499            }
500            for (final Account account : accounts) {
501                Intent i = new Intent(mContext, AccountSettingsActivity.class)
502                        .putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account.name);
503                i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
504                row.add(new MenuItem.Builder().id(mNextItemId++).title(account.name)
505                        .imageUriGetter(new AccountImageUriGetter(mContext, account))
506                        .intent(i)
507                        .build());
508            }
509        }
510
511        if (canAddAccount() && (mAllowMultipleAccounts || googleAccountCount == 0)) {
512            ComponentName componentName = new ComponentName("com.android.tv.settings",
513                    "com.android.tv.settings.accounts.AddAccountWithTypeActivity");
514            Intent i = new Intent().setComponent(componentName);
515            i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
516            if (accountTypes.length == 1) {
517                i.putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountTypes[0]);
518            }
519            row.add(new MenuItem.Builder().id(mNextItemId++)
520                    .title(mContext.getString(R.string.add_account))
521                    .imageResourceId(mContext, R.drawable.ic_settings_add)
522                    .intent(i).build());
523        }
524    }
525
526    private void addAccessories(ArrayObjectAdapter row) {
527        if (mBtAdapter != null) {
528            Set<BluetoothDevice> bondedDevices = mBtAdapter.getBondedDevices();
529            if (DEBUG) {
530                Log.d(TAG, "List of Bonded BT Devices:");
531            }
532
533            for (BluetoothDevice device : bondedDevices) {
534                if (DEBUG) {
535                    Log.d(TAG, "   Device name: " + device.getName() + " , Class: " +
536                            device.getBluetoothClass().getDeviceClass());
537                }
538
539                int resourceId = AccessoryUtils.getImageIdForDevice(device);
540                Intent i = BluetoothAccessoryActivity.getIntent(mContext, device.getAddress(),
541                        device.getName(), resourceId);
542                i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
543
544                String desc = isDeviceConnected(device) ? mContext.getString(
545                        R.string.accessory_connected)
546                        : null;
547
548                row.add(new MenuItem.Builder().id(mNextItemId++).title(device.getName())
549                        .description(desc).imageResourceId(mContext, resourceId)
550                        .intent(i).build());
551            }
552        }
553    }
554}
555