• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
18 
19 import android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.res.TypedArray;
27 import android.content.res.XmlResourceParser;
28 import android.media.tv.TvInputInfo;
29 import android.media.tv.TvInputManager;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.preference.PreferenceActivity;
33 import android.support.v17.leanback.widget.ArrayObjectAdapter;
34 import android.support.v17.leanback.widget.HeaderItem;
35 import android.support.v17.leanback.widget.ObjectAdapter;
36 import android.support.v17.leanback.widget.ListRow;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.util.TypedValue;
40 import android.util.Xml;
41 
42 import com.android.internal.util.XmlUtils;
43 
44 import com.android.tv.settings.accessories.AccessoryUtils;
45 import com.android.tv.settings.accessories.BluetoothAccessoryActivity;
46 import com.android.tv.settings.accounts.AccountImageUriGetter;
47 import com.android.tv.settings.accounts.AccountSettingsActivity;
48 import com.android.tv.settings.accounts.AuthenticatorHelper;
49 import com.android.tv.settings.connectivity.ConnectivityStatusIconUriGetter;
50 import com.android.tv.settings.connectivity.ConnectivityStatusTextGetter;
51 import com.android.tv.settings.connectivity.WifiNetworksActivity;
52 import com.android.tv.settings.device.sound.SoundActivity;
53 import com.android.tv.settings.users.RestrictedProfileActivity;
54 import com.android.tv.settings.util.UriUtils;
55 
56 import org.xmlpull.v1.XmlPullParser;
57 import org.xmlpull.v1.XmlPullParserException;
58 
59 import java.io.IOException;
60 import java.util.HashSet;
61 import java.util.Set;
62 
63 /**
64  * Gets the list of browse headers and browse items.
65  */
66 public 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 {
handleRequestedNode(Context context, XmlResourceParser parser, AttributeSet attrs)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 
SoundActivityImageUriGetter(Context context)88         SoundActivityImageUriGetter(Context context) {
89             mContext = context;
90         }
91 
92         @Override
getUri()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 
XmlReader(Context context, int xmlResource, String rootNodeName, String nodeNameRequested, XmlReaderListener listener)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 
read()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 
BrowseInfo(Context context)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
refreshContent()206     public void refreshContent() {
207         init();
208     }
209 
init()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 
checkForDeveloperOptionUpdate()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
handleRequestedNode(Context context, XmlResourceParser parser, AttributeSet attrs)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 
canAddAccount()255     private boolean canAddAccount() {
256         return !isRestricted();
257     }
258 
isRestricted()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 
PreferenceXmlReaderListener(int headerId, ArrayObjectAdapter row)268         PreferenceXmlReaderListener(int headerId, ArrayObjectAdapter row) {
269             mHeaderId = headerId;
270             mRow = row;
271         }
272 
273         @Override
handleRequestedNode(Context context, XmlResourceParser parser, AttributeSet attrs)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 
refreshWifiCard()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 
rebuildInfo()322     void rebuildInfo() {
323         init();
324     }
325 
updateAccounts()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 
updateAccessories()359     void updateAccessories() {
360         synchronized (mGuard) {
361             updateAccessories(R.id.accessories);
362         }
363     }
364 
updateWifi()365     public void updateWifi() {
366         mHandler.post(refreshWifiCardRunnable);
367     }
368 
bluetoothDeviceConnected(BluetoothDevice device)369     void bluetoothDeviceConnected(BluetoothDevice device) {
370         synchronized (mConnectedDevices) {
371             mConnectedDevices.add(device);
372         }
373     }
374 
bluetoothDeviceDisconnected(BluetoothDevice device)375     void bluetoothDeviceDisconnected(BluetoothDevice device) {
376         synchronized (mConnectedDevices) {
377             mConnectedDevices.remove(device);
378         }
379     }
380 
isDeviceConnected(BluetoothDevice device)381     boolean isDeviceConnected(BluetoothDevice device) {
382         synchronized (mConnectedDevices) {
383             return mConnectedDevices.contains(device);
384         }
385     }
386 
isInputSettingNeeded()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 
updateAccessories(int headerId)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 
getIntent(XmlResourceParser parser, AttributeSet attrs, int headerId)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 
getStringFromTypedArray(TypedArray sa, int resourceId)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 
getDescriptionTextGetterFromKey(String key)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 
getIconUriGetterFromKey(String key)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 
addAccounts(ArrayObjectAdapter row)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 
addAccessories(ArrayObjectAdapter row)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