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 
17 package com.android.tv.settings;
18 
19 import android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.content.ActivityNotFoundException;
24 import android.content.BroadcastReceiver;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.PackageManager;
31 import android.content.pm.ResolveInfo;
32 import android.os.Bundle;
33 import android.service.settings.suggestions.Suggestion;
34 import android.telephony.SignalStrength;
35 import android.util.Log;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.ViewGroup;
39 
40 import androidx.annotation.Keep;
41 import androidx.annotation.VisibleForTesting;
42 import androidx.preference.Preference;
43 import androidx.preference.PreferenceCategory;
44 import androidx.preference.SwitchPreference;
45 
46 import com.android.settingslib.core.AbstractPreferenceController;
47 import com.android.settingslib.suggestions.SuggestionControllerMixin;
48 import com.android.settingslib.utils.IconCache;
49 import com.android.tv.settings.HotwordSwitchController.HotwordStateListener;
50 import com.android.tv.settings.accounts.AccountsFragment;
51 import com.android.tv.settings.connectivity.ConnectivityListener;
52 import com.android.tv.settings.suggestions.SuggestionPreference;
53 import com.android.tv.settings.system.SecurityFragment;
54 
55 import java.util.ArrayList;
56 import java.util.List;
57 import java.util.Set;
58 
59 /**
60  * The fragment where all good things begin. Evil is handled elsewhere.
61  */
62 @Keep
63 public class MainFragment extends PreferenceControllerFragment implements
64         SuggestionControllerMixin.SuggestionControllerHost, SuggestionPreference.Callback,
65         HotwordStateListener {
66 
67     private static final String TAG = "MainFragment";
68     private static final String KEY_SUGGESTIONS_LIST = "suggestions";
69     @VisibleForTesting
70     static final String KEY_ACCOUNTS_AND_SIGN_IN = "accounts_and_sign_in";
71     private static final String KEY_APPLICATIONS = "applications";
72     @VisibleForTesting
73     static final String KEY_ACCESSORIES = "remotes_and_accessories";
74     @VisibleForTesting
75     static final String KEY_NETWORK = "network";
76 
77     @VisibleForTesting
78     static final String KEY_QUICK_SETTINGS = "quick_settings";
79 
80     @VisibleForTesting
81     ConnectivityListener mConnectivityListener;
82     @VisibleForTesting
83     PreferenceCategory mSuggestionsList;
84     private SuggestionControllerMixin mSuggestionControllerMixin;
85     @VisibleForTesting
86     IconCache mIconCache;
87     @VisibleForTesting
88     BluetoothAdapter mBtAdapter;
89     @VisibleForTesting
90     boolean mHasBtAccessories;
91     @VisibleForTesting
92     boolean mHasAccounts;
93 
94     /** Controllers for the Quick Settings section. */
95     private List<AbstractPreferenceController> mPreferenceControllers;
96     private HotwordSwitchController mHotwordSwitchController;
97     private PreferenceCategory mQuickSettingsList;
98     private SwitchPreference mHotwordSwitch;
99 
100     private final BroadcastReceiver mBCMReceiver = new BroadcastReceiver() {
101         @Override
102         public void onReceive(Context context, Intent intent) {
103             updateAccessoryPref();
104         }
105     };
106 
newInstance()107     public static MainFragment newInstance() {
108         return new MainFragment();
109     }
110 
111     @Override
getMetricsCategory()112     public int getMetricsCategory() {
113         // Do not log visibility.
114         return METRICS_CATEGORY_UNKNOWN;
115     }
116 
117     @Override
getPreferenceScreenResId()118     protected int getPreferenceScreenResId() {
119         return R.xml.main_prefs;
120     }
121 
122     @Override
onCreate(Bundle savedInstanceState)123     public void onCreate(Bundle savedInstanceState) {
124         mIconCache = new IconCache(getContext());
125         mConnectivityListener =
126                 new ConnectivityListener(getContext(), this::updateWifi, getLifecycle());
127         mBtAdapter = BluetoothAdapter.getDefaultAdapter();
128         super.onCreate(savedInstanceState);
129     }
130 
131     @Override
onDestroy()132     public void onDestroy() {
133         if (mHotwordSwitchController != null) {
134             mHotwordSwitchController.unregister();
135         }
136         super.onDestroy();
137     }
138 
139     /** @return true if there is at least one available item in quick settings. */
shouldShowQuickSettings()140     private boolean shouldShowQuickSettings() {
141         for (AbstractPreferenceController controller : mPreferenceControllers) {
142             if (controller.isAvailable()) {
143                 return true;
144             }
145         }
146         return false;
147     }
148 
showOrHideQuickSettings()149     private void showOrHideQuickSettings() {
150         if (shouldShowQuickSettings()) {
151             showQuickSettings();
152         } else {
153             hideQuickSettings();
154         }
155     }
156 
157     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)158     public View onCreateView(LayoutInflater inflater, ViewGroup container,
159             Bundle savedInstanceState) {
160         showOrHideQuickSettings();
161         return super.onCreateView(inflater, container, savedInstanceState);
162     }
163 
164     /** Creates the quick settings category and its children. */
showQuickSettings()165     private void showQuickSettings() {
166         if (mQuickSettingsList != null) {
167             return;
168         }
169         mQuickSettingsList = new PreferenceCategory(this.getPreferenceManager().getContext());
170         mQuickSettingsList.setKey(KEY_QUICK_SETTINGS);
171         mQuickSettingsList.setTitle(R.string.header_category_quick_settings);
172         mQuickSettingsList.setLayoutResource(R.layout.preference_category_compact_layout);
173         mQuickSettingsList.setOrder(1); // at top, but below suggested settings
174         getPreferenceScreen().addPreference(mQuickSettingsList);
175         mHotwordSwitch = new SwitchPreference(this.getPreferenceManager().getContext());
176         mHotwordSwitch.setKey(HotwordSwitchController.KEY_HOTWORD_SWITCH);
177         mHotwordSwitchController.updateState(mHotwordSwitch);
178         mQuickSettingsList.addPreference(mHotwordSwitch);
179     }
180 
181     /** Removes the quick settings category and all its children. */
hideQuickSettings()182     private void hideQuickSettings() {
183         Preference quickSettingsPref = findPreference(KEY_QUICK_SETTINGS);
184         if (quickSettingsPref == null) {
185             return;
186         }
187         mQuickSettingsList.removeAll();
188         getPreferenceScreen().removePreference(mQuickSettingsList);
189         mQuickSettingsList = null;
190     }
191 
192     @Override
onHotwordStateChanged()193     public void onHotwordStateChanged() {
194         if (mHotwordSwitch != null) {
195             mHotwordSwitchController.updateState(mHotwordSwitch);
196         }
197         showOrHideQuickSettings();
198     }
199 
200     @Override
onHotwordEnable()201     public void onHotwordEnable() {
202         try {
203             Intent intent = new Intent(HotwordSwitchController.ACTION_HOTWORD_ENABLE);
204             intent.setPackage(HotwordSwitchController.ASSISTANT_PGK_NAME);
205             startActivityForResult(intent, 0);
206         } catch (ActivityNotFoundException e) {
207             Log.w(TAG, "Unable to find hotwording activity.", e);
208         }
209     }
210 
211     @Override
onHotwordDisable()212     public void onHotwordDisable() {
213         try {
214             Intent intent = new Intent(HotwordSwitchController.ACTION_HOTWORD_DISABLE);
215             intent.setPackage(HotwordSwitchController.ASSISTANT_PGK_NAME);
216             startActivityForResult(intent, 0);
217         } catch (ActivityNotFoundException e) {
218             Log.w(TAG, "Unable to find hotwording activity.", e);
219         }
220     }
221 
222     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)223     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
224         setPreferencesFromResource(R.xml.main_prefs, null);
225         if (isRestricted()) {
226             Preference appPref = findPreference(KEY_APPLICATIONS);
227             if (appPref != null) {
228                 appPref.setVisible(false);
229             }
230             Preference accountsPref = findPreference(KEY_ACCOUNTS_AND_SIGN_IN);
231             if (accountsPref != null) {
232                 accountsPref.setVisible(false);
233             }
234         }
235         if (!supportBluetooth()) {
236             Preference accessoryPreference = findPreference(KEY_ACCESSORIES);
237             if (accessoryPreference != null) {
238                 accessoryPreference.setVisible(false);
239             }
240         }
241         mHotwordSwitchController.init(this);
242     }
243 
244     @Override
onCreatePreferenceControllers(Context context)245     protected List<AbstractPreferenceController> onCreatePreferenceControllers(Context context) {
246         mPreferenceControllers = new ArrayList<>(1);
247         mHotwordSwitchController = new HotwordSwitchController(context);
248         mPreferenceControllers.add(mHotwordSwitchController);
249         return mPreferenceControllers;
250     }
251 
252     @VisibleForTesting
updateWifi()253     void updateWifi() {
254         final Preference networkPref = findPreference(KEY_NETWORK);
255         if (networkPref == null) {
256             return;
257         }
258 
259         if (mConnectivityListener.isCellConnected()) {
260             final int signal = mConnectivityListener.getCellSignalStrength();
261             switch (signal) {
262                 case SignalStrength.SIGNAL_STRENGTH_GREAT:
263                     networkPref.setIcon(R.drawable.ic_cell_signal_4_white);
264                     break;
265                 case SignalStrength.SIGNAL_STRENGTH_GOOD:
266                     networkPref.setIcon(R.drawable.ic_cell_signal_3_white);
267                     break;
268                 case SignalStrength.SIGNAL_STRENGTH_MODERATE:
269                     networkPref.setIcon(R.drawable.ic_cell_signal_2_white);
270                     break;
271                 case SignalStrength.SIGNAL_STRENGTH_POOR:
272                     networkPref.setIcon(R.drawable.ic_cell_signal_1_white);
273                     break;
274                 case SignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN:
275                 default:
276                     networkPref.setIcon(R.drawable.ic_cell_signal_0_white);
277                     break;
278             }
279         } else if (mConnectivityListener.isEthernetConnected()) {
280             networkPref.setIcon(R.drawable.ic_ethernet_white);
281             networkPref.setSummary(R.string.connectivity_summary_ethernet_connected);
282         } else if (mConnectivityListener.isWifiEnabledOrEnabling()) {
283             if (mConnectivityListener.isWifiConnected()) {
284                 final int signal = mConnectivityListener.getWifiSignalStrength(5);
285                 switch (signal) {
286                     case 4:
287                         networkPref.setIcon(R.drawable.ic_wifi_signal_4_white);
288                         break;
289                     case 3:
290                         networkPref.setIcon(R.drawable.ic_wifi_signal_3_white);
291                         break;
292                     case 2:
293                         networkPref.setIcon(R.drawable.ic_wifi_signal_2_white);
294                         break;
295                     case 1:
296                         networkPref.setIcon(R.drawable.ic_wifi_signal_1_white);
297                         break;
298                     case 0:
299                     default:
300                         networkPref.setIcon(R.drawable.ic_wifi_signal_0_white);
301                         break;
302                 }
303                 networkPref.setSummary(mConnectivityListener.getSsid());
304             } else {
305                 networkPref.setIcon(R.drawable.ic_wifi_not_connected);
306                 networkPref.setSummary(R.string.connectivity_summary_no_network_connected);
307             }
308         } else {
309             networkPref.setIcon(R.drawable.ic_wifi_signal_off_white);
310             networkPref.setSummary(R.string.connectivity_summary_wifi_disabled);
311         }
312     }
313 
314     /**
315      * Returns the ResolveInfo for the system activity that matches given intent filter or null if
316      * no such activity exists.
317      * @param context Context of the caller
318      * @param intent The intent matching the desired system app
319      * @return ResolveInfo of the matching activity or null if no match exists
320      */
systemIntentIsHandled(Context context, Intent intent)321     public static ResolveInfo systemIntentIsHandled(Context context, Intent intent) {
322         if (intent == null) {
323             return null;
324         }
325 
326         final PackageManager pm = context.getPackageManager();
327 
328         for (ResolveInfo info : pm.queryIntentActivities(intent, 0)) {
329             if (info.activityInfo != null
330                     && (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
331                     == ApplicationInfo.FLAG_SYSTEM) {
332                 return info;
333             }
334         }
335         return null;
336     }
337 
338     @Override
onSuggestionReady(List<Suggestion> data)339     public void onSuggestionReady(List<Suggestion> data) {
340         if (data == null || data.size() == 0) {
341             if (mSuggestionsList != null) {
342                 getPreferenceScreen().removePreference(mSuggestionsList);
343                 mSuggestionsList = null;
344             }
345             return;
346         }
347 
348         if (mSuggestionsList == null) {
349             mSuggestionsList = new PreferenceCategory(this.getPreferenceManager().getContext());
350             mSuggestionsList.setKey(KEY_SUGGESTIONS_LIST);
351             mSuggestionsList.setTitle(R.string.header_category_suggestions);
352             mSuggestionsList.setLayoutResource(R.layout.preference_category_compact_layout);
353             mSuggestionsList.setOrder(0); // always at top
354             getPreferenceScreen().addPreference(mSuggestionsList);
355         }
356         updateSuggestionList(data);
357     }
358 
359     @VisibleForTesting
updateSuggestionList(List<Suggestion> suggestions)360     void updateSuggestionList(List<Suggestion> suggestions) {
361         // Remove suggestions that are not in the new list.
362         for (int i = 0; i < mSuggestionsList.getPreferenceCount(); i++) {
363             SuggestionPreference pref = (SuggestionPreference) mSuggestionsList.getPreference(i);
364             boolean isInNewSuggestionList = false;
365             for (Suggestion suggestion : suggestions) {
366                 if (pref.getId().equals(suggestion.getId())) {
367                     isInNewSuggestionList = true;
368                     break;
369                 }
370             }
371             if (!isInNewSuggestionList) {
372                 mSuggestionsList.removePreference(pref);
373             }
374         }
375 
376         // Add suggestions that are not in the old list and update the existing suggestions.
377         for (Suggestion suggestion : suggestions) {
378             Preference curPref = findPreference(
379                         SuggestionPreference.SUGGESTION_PREFERENCE_KEY + suggestion.getId());
380             if (curPref == null) {
381                 SuggestionPreference newSuggPref = new SuggestionPreference(
382                         suggestion, this.getPreferenceManager().getContext(),
383                         mSuggestionControllerMixin, this);
384                 newSuggPref.setIcon(mIconCache.getIcon(suggestion.getIcon()));
385                 newSuggPref.setTitle(suggestion.getTitle());
386                 newSuggPref.setSummary(suggestion.getSummary());
387                 mSuggestionsList.addPreference(newSuggPref);
388             } else {
389                 // Even though the id of suggestion might not change, the details could change.
390                 // So we need to update icon, title and summary for the suggestions.
391                 curPref.setIcon(mIconCache.getIcon(suggestion.getIcon()));
392                 curPref.setTitle(suggestion.getTitle());
393                 curPref.setSummary(suggestion.getSummary());
394             }
395         }
396     }
397 
398     @Override
onResume()399     public void onResume() {
400         super.onResume();
401         updateAccountPref();
402         updateAccessoryPref();
403     }
404 
isRestricted()405     private boolean isRestricted() {
406         return SecurityFragment.isRestrictedProfileInEffect(getContext());
407     }
408 
409     @VisibleForTesting
updateAccessoryPref()410     void updateAccessoryPref() {
411         Preference accessoryPreference = findPreference(KEY_ACCESSORIES);
412         if (mBtAdapter == null || accessoryPreference == null) {
413             return;
414         }
415 
416         final Set<BluetoothDevice> bondedDevices = mBtAdapter.getBondedDevices();
417         if (bondedDevices.size() == 0) {
418             mHasBtAccessories = false;
419         } else {
420             mHasBtAccessories = true;
421         }
422     }
423 
424     @VisibleForTesting
updateAccountPref()425     void updateAccountPref() {
426         final Preference accountsPref = findPreference(KEY_ACCOUNTS_AND_SIGN_IN);
427         if (accountsPref != null && accountsPref.isVisible()) {
428             final AccountManager am = AccountManager.get(getContext());
429             Account[] accounts = am.getAccounts();
430             if (accounts.length == 0) {
431                 mHasAccounts = false;
432                 accountsPref.setIcon(R.drawable.ic_add_an_account);
433                 accountsPref.setSummary(R.string.accounts_category_summary_no_account);
434                 AccountsFragment.setUpAddAccountPrefIntent(accountsPref, getContext());
435             } else {
436                 mHasAccounts = true;
437                 accountsPref.setIcon(R.drawable.ic_accounts_and_sign_in);
438                 if (accounts.length == 1) {
439                     accountsPref.setSummary(accounts[0].name);
440                 } else {
441                     accountsPref.setSummary(getResources().getQuantityString(
442                             R.plurals.accounts_category_summary, accounts.length, accounts.length));
443                 }
444             }
445         }
446     }
447 
448     @Override
onStart()449     public void onStart() {
450         super.onStart();
451         IntentFilter btChangeFilter = new IntentFilter();
452         btChangeFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
453         btChangeFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
454         btChangeFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
455         getContext().registerReceiver(mBCMReceiver, btChangeFilter);
456     }
457 
458     @Override
onStop()459     public void onStop() {
460         super.onStop();
461         getContext().unregisterReceiver(mBCMReceiver);
462     }
463 
464     @Override
onAttach(Context context)465     public void onAttach(Context context) {
466         super.onAttach(context);
467         ComponentName componentName = new ComponentName(
468                 "com.android.settings.intelligence",
469                 "com.android.settings.intelligence.suggestions.SuggestionService");
470         mSuggestionControllerMixin = new SuggestionControllerMixin(
471                                             context, this, getLifecycle(), componentName);
472     }
473 
474     @Override
onPreferenceTreeClick(Preference preference)475     public boolean onPreferenceTreeClick(Preference preference) {
476         if (preference.getKey().equals(KEY_ACCOUNTS_AND_SIGN_IN) && !mHasAccounts
477                 || (preference.getKey().equals(KEY_ACCESSORIES) && !mHasBtAccessories)) {
478             getContext().startActivity(preference.getIntent());
479             return true;
480         } else {
481             return super.onPreferenceTreeClick(preference);
482         }
483     }
484 
485     @Override
onSuggestionClosed(Preference preference)486     public void onSuggestionClosed(Preference preference) {
487         if (mSuggestionsList == null || mSuggestionsList.getPreferenceCount() == 0) {
488             return;
489         } else if (mSuggestionsList.getPreferenceCount() == 1) {
490             getPreferenceScreen().removePreference(mSuggestionsList);
491         } else {
492             mSuggestionsList.removePreference(preference);
493         }
494     }
495 
supportBluetooth()496     private boolean supportBluetooth() {
497         return getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
498                 ? true
499                 : false;
500     }
501 }
502