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