1/*
2 * Copyright (C) 2015 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.BroadcastReceiver;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.content.pm.ApplicationInfo;
30import android.content.pm.PackageManager;
31import android.content.pm.ResolveInfo;
32import android.content.res.Resources;
33import android.graphics.drawable.Drawable;
34import android.media.tv.TvInputInfo;
35import android.media.tv.TvInputManager;
36import android.os.Bundle;
37import android.os.UserHandle;
38import android.support.v17.preference.LeanbackPreferenceFragment;
39import android.support.v4.content.LocalBroadcastManager;
40import android.support.v7.preference.Preference;
41import android.support.v7.preference.PreferenceGroup;
42import android.text.TextUtils;
43import android.util.ArraySet;
44import android.util.Log;
45
46import com.android.settingslib.accounts.AuthenticatorHelper;
47import com.android.tv.settings.accessories.AccessoryUtils;
48import com.android.tv.settings.accessories.BluetoothAccessoryFragment;
49import com.android.tv.settings.accessories.BluetoothConnectionsManager;
50import com.android.tv.settings.accounts.AccountSyncFragment;
51import com.android.tv.settings.accounts.AddAccountWithTypeActivity;
52import com.android.tv.settings.connectivity.ConnectivityListener;
53import com.android.tv.settings.device.sound.SoundFragment;
54import com.android.tv.settings.system.SecurityFragment;
55
56import java.util.ArrayList;
57import java.util.Set;
58
59public class MainFragment extends LeanbackPreferenceFragment {
60    private static final String TAG = "MainFragment";
61
62    private static final String KEY_DEVELOPER = "developer";
63    private static final String KEY_LOCATION = "location";
64    private static final String KEY_SECURITY = "security";
65    private static final String KEY_USAGE = "usageAndDiag";
66    private static final String KEY_ADD_ACCOUNT = "add_account";
67    private static final String KEY_ACCESSORIES = "accessories";
68    private static final String KEY_PERSONAL = "personal";
69    private static final String KEY_ADD_ACCESSORY = "add_accessory";
70    private static final String KEY_NETWORK = "network";
71    private static final String KEY_INPUTS = "inputs";
72    private static final String KEY_SOUNDS = "sound_effects";
73    private static final String KEY_GOOGLE_SETTINGS = "googleSettings";
74    private static final String KEY_HOME_SETTINGS = "home";
75    private static final String KEY_CAST_SETTINGS = "cast";
76    private static final String KEY_SPEECH_SETTINGS = "speech";
77    private static final String KEY_SEARCH_SETTINGS = "search";
78    private static final String KEY_ACCOUNTS_CATEGORY = "accounts";
79
80    private AuthenticatorHelper mAuthenticatorHelper;
81    private BluetoothAdapter mBtAdapter;
82    private ConnectivityListener mConnectivityListener;
83
84    private boolean mInputSettingNeeded;
85
86    private Preference mDeveloperPref;
87    private PreferenceGroup mAccessoriesGroup;
88    private PreferenceGroup mAccountsGroup;
89    private Preference mAddAccessory;
90    private Preference mNetworkPref;
91    private Preference mSoundsPref;
92
93    private final BroadcastReceiver mBCMReceiver = new BroadcastReceiver() {
94        @Override
95        public void onReceive(Context context, Intent intent) {
96            updateAccessories();
97        }
98    };
99
100    public static MainFragment newInstance() {
101        return new MainFragment();
102    }
103
104    @Override
105    public void onCreate(Bundle savedInstanceState) {
106        mAuthenticatorHelper = new AuthenticatorHelper(getContext(),
107                new UserHandle(UserHandle.myUserId()),
108                new AuthenticatorHelper.OnAccountsUpdateListener() {
109                    @Override
110                    public void onAccountsUpdate(UserHandle userHandle) {
111                        updateAccounts();
112                    }
113                });
114        mBtAdapter = BluetoothAdapter.getDefaultAdapter();
115        mConnectivityListener = new ConnectivityListener(getContext(),
116                new ConnectivityListener.Listener() {
117                    @Override
118                    public void onConnectivityChange() {
119                        updateWifi();
120                    }
121                });
122
123        final TvInputManager manager = (TvInputManager) getContext().getSystemService(
124                Context.TV_INPUT_SERVICE);
125        if (manager != null) {
126            for (final TvInputInfo input : manager.getTvInputList()) {
127                if (input.isPassthroughInput()) {
128                    mInputSettingNeeded = true;
129                }
130            }
131        }
132        super.onCreate(savedInstanceState);
133    }
134
135    @Override
136    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
137        if (isRestricted()) {
138            setPreferencesFromResource(R.xml.restricted_prefs, null);
139        } else {
140            setPreferencesFromResource(R.xml.main_prefs, null);
141        }
142        mDeveloperPref = findPreference(KEY_DEVELOPER);
143        mAccessoriesGroup = (PreferenceGroup) findPreference(KEY_ACCESSORIES);
144        mAddAccessory = findPreference(KEY_ADD_ACCESSORY);
145        mNetworkPref = findPreference(KEY_NETWORK);
146        mSoundsPref = findPreference(KEY_SOUNDS);
147        mAccountsGroup = (PreferenceGroup) findPreference(KEY_ACCOUNTS_CATEGORY);
148
149        final Preference inputPref = findPreference(KEY_INPUTS);
150        if (inputPref != null) {
151            inputPref.setVisible(mInputSettingNeeded);
152        }
153    }
154
155    @Override
156    public void onStart() {
157        super.onStart();
158        mAuthenticatorHelper.listenToAccountUpdates();
159        final IntentFilter filter =
160                new IntentFilter(BluetoothConnectionsManager.ACTION_BLUETOOTH_UPDATE);
161        LocalBroadcastManager.getInstance(getContext()).registerReceiver(mBCMReceiver, filter);
162    }
163
164    @Override
165    public void onResume() {
166        super.onResume();
167
168        updateAccounts();
169        updateAccessories();
170        updateDeveloperOptions();
171        updateSounds();
172        updateGoogleSettings();
173
174        hideIfIntentUnhandled(findPreference(KEY_HOME_SETTINGS));
175        hideIfIntentUnhandled(findPreference(KEY_CAST_SETTINGS));
176        hideIfIntentUnhandled(findPreference(KEY_USAGE));
177        hideIfIntentUnhandled(findPreference(KEY_SPEECH_SETTINGS));
178        hideIfIntentUnhandled(findPreference(KEY_SEARCH_SETTINGS));
179    }
180
181    @Override
182    public void onStop() {
183        super.onStop();
184        mAuthenticatorHelper.stopListeningToAccountUpdates();
185        LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mBCMReceiver);
186    }
187
188    private void hideIfIntentUnhandled(Preference preference) {
189        if (preference == null) {
190            return;
191        }
192        preference.setVisible(systemIntentIsHandled(preference.getIntent()) != null);
193    }
194
195    private boolean isRestricted() {
196        return SecurityFragment.isRestrictedProfileInEffect(getContext());
197    }
198
199    private void updateAccounts() {
200        if (mAccountsGroup == null) {
201            return;
202        }
203
204        final Set<String> touchedAccounts = new ArraySet<>(mAccountsGroup.getPreferenceCount());
205
206        final AccountManager am = AccountManager.get(getContext());
207        final AuthenticatorDescription[] authTypes = am.getAuthenticatorTypes();
208        final ArrayList<String> allowableAccountTypes = new ArrayList<>(authTypes.length);
209        final Context themedContext = getPreferenceManager().getContext();
210
211        for (AuthenticatorDescription authDesc : authTypes) {
212            final Context targetContext;
213            try {
214                targetContext = getContext().createPackageContext(authDesc.packageName, 0);
215            } catch (PackageManager.NameNotFoundException e) {
216                Log.e(TAG, "Authenticator description with bad package name", e);
217                continue;
218            } catch (SecurityException e) {
219                Log.e(TAG, "Security exception loading package resources", e);
220                continue;
221            }
222
223            // Main title text comes from the authenticator description (e.g. "Google").
224            String authTitle = null;
225            try {
226                authTitle = targetContext.getString(authDesc.labelId);
227                if (TextUtils.isEmpty(authTitle)) {
228                    authTitle = null;  // Handled later when we add the row.
229                }
230            } catch (Resources.NotFoundException e) {
231                Log.e(TAG, "Authenticator description with bad label id", e);
232            }
233
234            // There exist some authenticators which aren't intended to be user-facing.
235            // If the authenticator doesn't have a title or an icon, don't present it to
236            // the user as an option.
237            if (authTitle != null || authDesc.iconId != 0) {
238                allowableAccountTypes.add(authDesc.type);
239            }
240
241            Account[] accounts = am.getAccountsByType(authDesc.type);
242            if (accounts == null || accounts.length == 0) {
243                continue;  // No point in continuing; there aren't any accounts to show.
244            }
245
246            // Icon URI to be displayed for each account is based on the type of authenticator.
247            Drawable authImage = null;
248            try {
249                authImage = targetContext.getDrawable(authDesc.iconId);
250            } catch (Resources.NotFoundException e) {
251                Log.e(TAG, "Authenticator has bad resources", e);
252            }
253
254            // Display an entry for each installed account we have.
255            for (final Account account : accounts) {
256                final String key = "account_pref:" + account.type + ":" + account.name;
257                Preference preference = findPreference(key);
258                if (preference == null) {
259                    preference = new Preference(themedContext);
260                }
261                preference.setTitle(authTitle != null ? authTitle : account.name);
262                preference.setIcon(authImage);
263                preference.setSummary(authTitle != null ? account.name : null);
264                preference.setFragment(AccountSyncFragment.class.getName());
265                AccountSyncFragment.prepareArgs(preference.getExtras(), account);
266
267                touchedAccounts.add(key);
268                preference.setKey(key);
269
270                mAccountsGroup.addPreference(preference);
271            }
272        }
273
274        for (int i = 0; i < mAccountsGroup.getPreferenceCount();) {
275            final Preference preference = mAccountsGroup.getPreference(i);
276            final String key = preference.getKey();
277            if (touchedAccounts.contains(key) || TextUtils.equals(KEY_ADD_ACCOUNT, key)) {
278                i++;
279            } else {
280                mAccountsGroup.removePreference(preference);
281            }
282        }
283
284        // Never allow restricted profile to add accounts.
285        final Preference addAccountPref = findPreference(KEY_ADD_ACCOUNT);
286        if (addAccountPref != null) {
287            addAccountPref.setOrder(Integer.MAX_VALUE);
288            if (isRestricted()) {
289                addAccountPref.setVisible(false);
290            } else {
291                Intent i = new Intent().setComponent(new ComponentName("com.android.tv.settings",
292                        "com.android.tv.settings.accounts.AddAccountWithTypeActivity"));
293                i.putExtra(AddAccountWithTypeActivity.EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY,
294                        allowableAccountTypes.toArray(new String[allowableAccountTypes.size()]));
295
296                // If there are available account types, show the "add account" button.
297                addAccountPref.setVisible(!allowableAccountTypes.isEmpty());
298                addAccountPref.setIntent(i);
299            }
300        }
301    }
302
303    private void updateAccessories() {
304        if (mAccessoriesGroup == null) {
305            return;
306        }
307
308        if (mBtAdapter == null) {
309            mAccessoriesGroup.setVisible(false);
310            mAccessoriesGroup.removeAll();
311            return;
312        }
313
314        final Set<BluetoothDevice> bondedDevices = mBtAdapter.getBondedDevices();
315        final Set<String> connectedBluetoothAddresses =
316                BluetoothConnectionsManager.getConnectedSet(getContext());
317        final Context themedContext = getPreferenceManager().getContext();
318
319        final Set<String> touchedKeys = new ArraySet<>(bondedDevices.size() + 1);
320        if (mAddAccessory != null) {
321            touchedKeys.add(mAddAccessory.getKey());
322        }
323
324        for (final BluetoothDevice device : bondedDevices) {
325            final String desc = connectedBluetoothAddresses.contains(device.getAddress())
326                    ? getString(R.string.accessory_connected)
327                    : null;
328
329            final String key = "BluetoothDevice:" + device.getAddress();
330            touchedKeys.add(key);
331            Preference preference = mAccessoriesGroup.findPreference(key);
332            if (preference == null) {
333                preference = new Preference(themedContext);
334                preference.setKey(key);
335            }
336            preference.setTitle(device.getName());
337            preference.setSummary(desc);
338            final int deviceImgId = AccessoryUtils.getImageIdForDevice(device);
339            preference.setIcon(deviceImgId);
340            preference.setFragment(BluetoothAccessoryFragment.class.getName());
341            BluetoothAccessoryFragment.prepareArgs(
342                    preference.getExtras(),
343                    device.getAddress(),
344                    device.getName(),
345                    deviceImgId);
346            mAccessoriesGroup.addPreference(preference);
347        }
348
349        for (int i = 0; i < mAccessoriesGroup.getPreferenceCount();) {
350            final Preference preference = mAccessoriesGroup.getPreference(i);
351            if (touchedKeys.contains(preference.getKey())) {
352                i++;
353            } else {
354                mAccessoriesGroup.removePreference(preference);
355            }
356        }
357    }
358
359    private void updateDeveloperOptions() {
360        if (mDeveloperPref == null) {
361            return;
362        }
363
364        final boolean developerEnabled = PreferenceUtils.isDeveloperEnabled(getContext());
365        mDeveloperPref.setVisible(developerEnabled);
366    }
367
368    private void updateSounds() {
369        if (mSoundsPref == null) {
370            return;
371        }
372
373        mSoundsPref.setIcon(SoundFragment.getSoundEffectsEnabled(getContext().getContentResolver())
374                ? R.drawable.ic_volume_up : R.drawable.ic_volume_off);
375    }
376
377    private void updateWifi() {
378        if (mNetworkPref == null) {
379            return;
380        }
381
382        mNetworkPref.setTitle(mConnectivityListener.isEthernetAvailable()
383                ? R.string.connectivity_network : R.string.connectivity_wifi);
384
385        // TODO: signal strength
386    }
387
388    public void updateGoogleSettings() {
389        final Preference googleSettingsPref = findPreference(KEY_GOOGLE_SETTINGS);
390        if (googleSettingsPref != null) {
391            final ResolveInfo info = systemIntentIsHandled(googleSettingsPref.getIntent());
392            googleSettingsPref.setVisible(info != null);
393            if (info != null) {
394                try {
395                    final Context targetContext = getContext()
396                            .createPackageContext(info.resolvePackageName != null ?
397                                    info.resolvePackageName : info.activityInfo.packageName, 0);
398                    googleSettingsPref.setIcon(targetContext.getDrawable(info.iconResourceId));
399                } catch (Resources.NotFoundException | PackageManager.NameNotFoundException
400                        | SecurityException e) {
401                    Log.e(TAG, "Google settings icon not found", e);
402                }
403            }
404        }
405    }
406
407    private ResolveInfo systemIntentIsHandled(Intent intent) {
408        if (intent == null) {
409            return null;
410        }
411
412        final PackageManager pm = getContext().getPackageManager();
413
414        for (ResolveInfo info : pm.queryIntentActivities(intent, 0)) {
415            if (info.activityInfo != null && info.activityInfo.enabled &&
416                    (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) ==
417                            ApplicationInfo.FLAG_SYSTEM) {
418                return info;
419            }
420        }
421        return null;
422    }
423}
424