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