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