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