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