blob: e453ab60056e4c4cac24d213b9cde854a9c9b23d [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.settings;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.Intent.ShortcutIconResource;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceActivity;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ObjectAdapter;
import android.support.v17.leanback.widget.ListRow;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.util.Xml;
import com.android.internal.util.XmlUtils;
import com.android.tv.settings.accessories.AccessoryUtils;
import com.android.tv.settings.accessories.BluetoothAccessoryActivity;
import com.android.tv.settings.accessories.BluetoothConnectionsManager;
import com.android.tv.settings.accounts.AccountImageUriGetter;
import com.android.tv.settings.accounts.AccountSettingsActivity;
import com.android.tv.settings.accounts.AddAccountWithTypeActivity;
import com.android.tv.settings.accounts.AuthenticatorHelper;
import com.android.tv.settings.connectivity.ConnectivityStatusIconUriGetter;
import com.android.tv.settings.connectivity.ConnectivityStatusTextGetter;
import com.android.tv.settings.connectivity.WifiNetworksActivity;
import com.android.tv.settings.device.sound.SoundActivity;
import com.android.tv.settings.users.RestrictedProfileActivity;
import com.android.tv.settings.util.UriUtils;
import com.android.tv.settings.util.AccountImageHelper;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Set;
/**
* Gets the list of browse headers and browse items.
*/
public class BrowseInfo extends BrowseInfoBase {
private static final String TAG = "CanvasSettings.BrowseInfo";
private static final boolean DEBUG = false;
public static final String EXTRA_ACCESSORY_ADDRESS = "accessory_address";
public static final String EXTRA_ACCESSORY_NAME = "accessory_name";
public static final String EXTRA_ACCESSORY_ICON_ID = "accessory_icon_res";
private static final String ACCOUNT_TYPE_GOOGLE = "com.google";
private static final String ETHERNET_PREFERENCE_KEY = "ethernet";
interface XmlReaderListener {
void handleRequestedNode(Context context, XmlResourceParser parser, AttributeSet attrs)
throws org.xmlpull.v1.XmlPullParserException, IOException;
}
static class SoundActivityImageUriGetter implements MenuItem.UriGetter {
private final Context mContext;
SoundActivityImageUriGetter(Context context) {
mContext = context;
}
@Override
public String getUri() {
return UriUtils.getAndroidResourceUri(mContext.getResources(),
SoundActivity.getIconResource(mContext.getContentResolver()));
}
}
static class XmlReader {
private final Context mContext;
private final int mXmlResource;
private final String mRootNodeName;
private final String mNodeNameRequested;
private final XmlReaderListener mListener;
XmlReader(Context context, int xmlResource, String rootNodeName, String nodeNameRequested,
XmlReaderListener listener) {
mContext = context;
mXmlResource = xmlResource;
mRootNodeName = rootNodeName;
mNodeNameRequested = nodeNameRequested;
mListener = listener;
}
void read() {
XmlResourceParser parser = null;
try {
parser = mContext.getResources().getXml(mXmlResource);
AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& type != XmlPullParser.START_TAG) {
// Parse next until start tag is found
}
String nodeName = parser.getName();
if (!mRootNodeName.equals(nodeName)) {
throw new RuntimeException("XML document must start with <" + mRootNodeName
+ "> tag; found" + nodeName + " at " + parser.getPositionDescription());
}
Bundle curBundle = null;
final int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
nodeName = parser.getName();
if (mNodeNameRequested.equals(nodeName)) {
mListener.handleRequestedNode(mContext, parser, attrs);
} else {
XmlUtils.skipCurrentTag(parser);
}
}
} catch (XmlPullParserException e) {
throw new RuntimeException("Error parsing headers", e);
} catch (IOException e) {
throw new RuntimeException("Error parsing headers", e);
} finally {
if (parser != null)
parser.close();
}
}
}
private static final String PREF_KEY_ADD_ACCOUNT = "add_account";
private static final String PREF_KEY_ADD_ACCESSORY = "add_accessory";
private static final String PREF_KEY_WIFI = "network";
private static final String PREF_KEY_DEVELOPER = "developer";
private static final String PREF_KEY_INPUTS = "inputs";
private static final String PREF_KEY_HOME = "home";
private final Context mContext;
private final AuthenticatorHelper mAuthenticatorHelper;
private int mNextItemId;
private int mAccountHeaderId;
private final BluetoothAdapter mBtAdapter;
private final Object mGuard = new Object();
private MenuItem mWifiItem = null;
private ArrayObjectAdapter mWifiRow = null;
private final Handler mHandler = new Handler();
private PreferenceUtils mPreferenceUtils;
private boolean mDeveloperEnabled;
private boolean mInputSettingNeeded;
BrowseInfo(Context context) {
mContext = context;
mAuthenticatorHelper = new AuthenticatorHelper();
mAuthenticatorHelper.updateAuthDescriptions(context);
mAuthenticatorHelper.onAccountsUpdated(context, null);
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
mNextItemId = 0;
mPreferenceUtils = new PreferenceUtils(context);
mDeveloperEnabled = mPreferenceUtils.isDeveloperEnabled();
mInputSettingNeeded = isInputSettingNeeded();
}
void init() {
synchronized (mGuard) {
mHeaderItems.clear();
mRows.clear();
int settingsXml = isRestricted() ? R.xml.restricted_main : R.xml.main;
new XmlReader(mContext, settingsXml, "preference-headers", "header",
new HeaderXmlReaderListener()).read();
updateAccessories(R.id.accessories);
}
}
void checkForDeveloperOptionUpdate() {
final boolean developerEnabled = mPreferenceUtils.isDeveloperEnabled();
if (developerEnabled != mDeveloperEnabled) {
mDeveloperEnabled = developerEnabled;
init();
}
}
private class HeaderXmlReaderListener implements XmlReaderListener {
@Override
public void handleRequestedNode(Context context, XmlResourceParser parser,
AttributeSet attrs)
throws XmlPullParserException, IOException {
TypedArray sa = mContext.getResources().obtainAttributes(attrs,
com.android.internal.R.styleable.PreferenceHeader);
final int headerId = sa.getResourceId(
com.android.internal.R.styleable.PreferenceHeader_id,
(int) PreferenceActivity.HEADER_ID_UNDEFINED);
String title = getStringFromTypedArray(sa,
com.android.internal.R.styleable.PreferenceHeader_title);
sa.recycle();
sa = context.getResources().obtainAttributes(attrs, R.styleable.CanvasSettings);
int preferenceRes = sa.getResourceId(R.styleable.CanvasSettings_preference, 0);
sa.recycle();
mHeaderItems.add(new HeaderItem(headerId, title));
final ArrayObjectAdapter currentRow = new ArrayObjectAdapter();
mRows.put(headerId, currentRow);
if (headerId != R.id.accessories) {
new XmlReader(context, preferenceRes, "PreferenceScreen", "Preference",
new PreferenceXmlReaderListener(headerId, currentRow)).read();
}
}
}
private boolean isRestricted() {
return RestrictedProfileActivity.isRestrictedProfileInEffect(mContext);
}
private class PreferenceXmlReaderListener implements XmlReaderListener {
private final int mHeaderId;
private final ArrayObjectAdapter mRow;
PreferenceXmlReaderListener(int headerId, ArrayObjectAdapter row) {
mHeaderId = headerId;
mRow = row;
}
@Override
public void handleRequestedNode(Context context, XmlResourceParser parser,
AttributeSet attrs) throws XmlPullParserException, IOException {
TypedArray sa = context.getResources().obtainAttributes(attrs,
com.android.internal.R.styleable.Preference);
String key = getStringFromTypedArray(sa,
com.android.internal.R.styleable.Preference_key);
String title = getStringFromTypedArray(sa,
com.android.internal.R.styleable.Preference_title);
int iconRes = sa.getResourceId(com.android.internal.R.styleable.Preference_icon,
R.drawable.settings_default_icon);
sa.recycle();
if (PREF_KEY_ADD_ACCOUNT.equals(key)) {
mAccountHeaderId = mHeaderId;
addAccounts(mRow);
} else if (PREF_KEY_HOME.equals(key)) {
// Only show home screen setting if there's a system app to handle the intent.
Intent recIntent = getIntent(parser, attrs, mHeaderId);
if (systemIntentIsHandled(recIntent)) {
mRow.add(new MenuItem.Builder()
.id(mNextItemId++)
.title(title)
.imageResourceId(mContext, iconRes)
.intent(recIntent)
.build());
}
} else if ((!key.equals(PREF_KEY_DEVELOPER) || mDeveloperEnabled)
&& (!key.equals(PREF_KEY_INPUTS) || mInputSettingNeeded)) {
MenuItem.TextGetter descriptionGetter = getDescriptionTextGetterFromKey(key);
MenuItem.UriGetter uriGetter = getIconUriGetterFromKey(key);
MenuItem.Builder builder = new MenuItem.Builder().id(mNextItemId++).title(title)
.descriptionGetter(descriptionGetter)
.intent(getIntent(parser, attrs, mHeaderId));
if(uriGetter == null) {
builder.imageResourceId(mContext, iconRes);
} else {
builder.imageUriGetter(uriGetter);
}
if (key.equals(PREF_KEY_WIFI)) {
mWifiItem = builder.build();
mRow.add(mWifiItem);
mWifiRow = mRow;
} else {
mRow.add(builder.build());
}
}
}
}
void rebuildInfo() {
init();
}
void updateAccounts() {
synchronized (mGuard) {
if (isRestricted()) {
// We don't display the accounts in restricted mode
return;
}
ArrayObjectAdapter row = mRows.get(mAccountHeaderId);
// Clear any account row cards that are not "Location" or "Security".
String dontDelete[] = new String[2];
dontDelete[0] = mContext.getString(R.string.system_location);
dontDelete[1] = mContext.getString(R.string.system_security);
int i = 0;
while (i < row.size ()) {
MenuItem menuItem = (MenuItem) row.get(i);
String title = menuItem.getTitle ();
boolean deleteItem = true;
for (int j = 0; j < dontDelete.length; ++j) {
if (title.equals(dontDelete[j])) {
deleteItem = false;
break;
}
}
if (deleteItem) {
row.removeItems(i, 1);
} else {
++i;
}
}
// Add accounts to end of row.
addAccounts(row);
}
}
void updateAccessories() {
synchronized (mGuard) {
updateAccessories(R.id.accessories);
}
}
public void updateWifi(final boolean isEthernetAvailable) {
if (mWifiItem != null) {
int index = mWifiRow.indexOf(mWifiItem);
if (index >= 0) {
mWifiItem = new MenuItem.Builder().from(mWifiItem)
.title(mContext.getString(isEthernetAvailable
? R.string.connectivity_network : R.string.connectivity_wifi))
.build();
mWifiRow.replace(index, mWifiItem);
}
}
}
private boolean isInputSettingNeeded() {
TvInputManager manager = (TvInputManager) mContext.getSystemService(
Context.TV_INPUT_SERVICE);
if (manager != null) {
for (TvInputInfo input : manager.getTvInputList()) {
if (input.isPassthroughInput()) {
return true;
}
}
}
return false;
}
private void updateAccessories(int headerId) {
ArrayObjectAdapter row = mRows.get(headerId);
row.clear();
addAccessories(row);
// Add new accessory activity icon
ComponentName componentName = new ComponentName("com.android.tv.settings",
"com.android.tv.settings.accessories.AddAccessoryActivity");
Intent i = new Intent().setComponent(componentName);
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
row.add(new MenuItem.Builder().id(mNextItemId++)
.title(mContext.getString(R.string.accessories_add))
.imageResourceId(mContext, R.drawable.ic_settings_bluetooth)
.intent(i).build());
}
private Intent getIntent(XmlResourceParser parser, AttributeSet attrs, int headerId)
throws org.xmlpull.v1.XmlPullParserException, IOException {
Intent intent = null;
if (parser.next() == XmlPullParser.START_TAG && "intent".equals(parser.getName())) {
TypedArray sa = mContext.getResources()
.obtainAttributes(attrs, com.android.internal.R.styleable.Intent);
String targetClass = getStringFromTypedArray(
sa, com.android.internal.R.styleable.Intent_targetClass);
String targetPackage = getStringFromTypedArray(
sa, com.android.internal.R.styleable.Intent_targetPackage);
String action = getStringFromTypedArray(
sa, com.android.internal.R.styleable.Intent_action);
if (targetClass != null && targetPackage != null) {
ComponentName componentName = new ComponentName(targetPackage, targetClass);
intent = new Intent();
intent.setComponent(componentName);
} else if (action != null) {
intent = new Intent(action);
}
XmlUtils.skipCurrentTag(parser);
}
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
return intent;
}
private String getStringFromTypedArray(TypedArray sa, int resourceId) {
String value = null;
TypedValue tv = sa.peekValue(resourceId);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
value = mContext.getString(tv.resourceId);
} else {
value = tv.string.toString();
}
}
return value;
}
private MenuItem.TextGetter getDescriptionTextGetterFromKey(String key) {
if (WifiNetworksActivity.PREFERENCE_KEY.equals(key)) {
return ConnectivityStatusTextGetter.createWifiStatusTextGetter(mContext);
}
if (ETHERNET_PREFERENCE_KEY.equals(key)) {
return ConnectivityStatusTextGetter.createEthernetStatusTextGetter(mContext);
}
return null;
}
private MenuItem.UriGetter getIconUriGetterFromKey(String key) {
if (SoundActivity.getPreferenceKey().equals(key)) {
return new SoundActivityImageUriGetter(mContext);
}
if (WifiNetworksActivity.PREFERENCE_KEY.equals(key)) {
return ConnectivityStatusIconUriGetter.createWifiStatusIconUriGetter(mContext);
}
return null;
}
private void addAccounts(ArrayObjectAdapter row) {
AccountManager am = AccountManager.get(mContext);
AuthenticatorDescription[] authTypes = am.getAuthenticatorTypes();
ArrayList<String> allowableAccountTypes = new ArrayList<>(authTypes.length);
PackageManager pm = mContext.getPackageManager();
int googleAccountCount = 0;
for (AuthenticatorDescription authDesc : authTypes) {
Resources resources = null;
try {
resources = pm.getResourcesForApplication(authDesc.packageName);
} catch (NameNotFoundException e) {
Log.e(TAG, "Authenticator description with bad package name", e);
continue;
}
allowableAccountTypes.add(authDesc.type);
// Main title text comes from the authenticator description (e.g. "Google").
String authTitle = null;
try {
authTitle = resources.getString(authDesc.labelId);
if (TextUtils.isEmpty(authTitle)) {
authTitle = null; // Handled later when we add the row.
}
} catch (NotFoundException e) {
Log.e(TAG, "Authenticator description with bad label id", e);
}
Account[] accounts = am.getAccountsByType(authDesc.type);
// Icon URI to be displayed for each account is based on the type of authenticator.
String imageUri = null;
if (ACCOUNT_TYPE_GOOGLE.equals(authDesc.type)) {
googleAccountCount = accounts.length;
imageUri = googleAccountIconUri(mContext);
} else {
imageUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
authDesc.packageName + '/' +
resources.getResourceTypeName(authDesc.iconId) + '/' +
resources.getResourceEntryName(authDesc.iconId))
.toString();
}
// Display an entry for each installed account we have.
for (final Account account : accounts) {
Intent i = new Intent(mContext, AccountSettingsActivity.class)
.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account.name);
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
row.add(new MenuItem.Builder().id(mNextItemId++)
.title(authTitle != null ? authTitle : account.name)
.imageUri(imageUri)
.description(authTitle != null ? account.name : null)
.intent(i)
.build());
}
}
// Never allow restricted profile to add accounts.
if (!isRestricted()) {
// If there's already a Google account installed, disallow installing a second one.
if (googleAccountCount > 0) {
allowableAccountTypes.remove(ACCOUNT_TYPE_GOOGLE);
}
// If there are available account types, add the "add account" button.
if (!allowableAccountTypes.isEmpty()) {
Intent i = new Intent().setComponent(new ComponentName("com.android.tv.settings",
"com.android.tv.settings.accounts.AddAccountWithTypeActivity"));
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
i.putExtra(AddAccountWithTypeActivity.EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY,
allowableAccountTypes.toArray(new String[allowableAccountTypes.size()]));
row.add(new MenuItem.Builder().id(mNextItemId++)
.title(mContext.getString(R.string.add_account))
.imageResourceId(mContext, R.drawable.ic_settings_add)
.intent(i).build());
}
}
}
private void addAccessories(ArrayObjectAdapter row) {
if (mBtAdapter != null) {
Set<BluetoothDevice> bondedDevices = mBtAdapter.getBondedDevices();
if (DEBUG) {
Log.d(TAG, "List of Bonded BT Devices:");
}
Set<String> connectedBluetoothAddresses =
BluetoothConnectionsManager.getConnectedSet(mContext);
for (BluetoothDevice device : bondedDevices) {
if (DEBUG) {
Log.d(TAG, " Device name: " + device.getName() + " , Class: " +
device.getBluetoothClass().getDeviceClass());
}
int resourceId = AccessoryUtils.getImageIdForDevice(device);
Intent i = BluetoothAccessoryActivity.getIntent(mContext, device.getAddress(),
device.getName(), resourceId);
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
String desc = connectedBluetoothAddresses.contains(device.getAddress())
? mContext.getString(R.string.accessory_connected)
: null;
row.add(new MenuItem.Builder().id(mNextItemId++).title(device.getName())
.description(desc).imageResourceId(mContext, resourceId)
.intent(i).build());
}
}
}
private static String googleAccountIconUri(Context context) {
ShortcutIconResource iconResource = new ShortcutIconResource();
iconResource.packageName = context.getPackageName();
iconResource.resourceName = context.getResources().getResourceName(
R.drawable.ic_settings_google_account);
return UriUtils.getShortcutIconResourceUri(iconResource).toString();
}
private boolean systemIntentIsHandled(Intent intent) {
if (mContext == null || intent == null) {
return false;
}
PackageManager pm = mContext.getPackageManager();
if (pm == null) {
return false;
}
for (ResolveInfo info : pm.queryIntentActivities(intent, 0)) {
if (info.activityInfo != null && info.activityInfo.enabled &&
(info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) ==
ApplicationInfo.FLAG_SYSTEM) {
return true;
}
}
return false;
}
}