1 /* 2 * Copyright (C) 2011 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.settings.location; 18 19 import android.app.Activity; 20 import android.app.admin.DevicePolicyManager; 21 import android.content.BroadcastReceiver; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.location.SettingInjectorService; 27 import android.os.Bundle; 28 import android.os.UserHandle; 29 import android.os.UserManager; 30 import android.provider.Settings; 31 import android.support.v7.preference.Preference; 32 import android.support.v7.preference.PreferenceCategory; 33 import android.support.v7.preference.PreferenceGroup; 34 import android.support.v7.preference.PreferenceScreen; 35 import android.util.Log; 36 import android.view.Menu; 37 import android.view.MenuInflater; 38 import android.view.MenuItem; 39 import android.widget.Switch; 40 import com.android.internal.logging.MetricsProto.MetricsEvent; 41 import com.android.settings.DimmableIconPreference; 42 import com.android.settings.R; 43 import com.android.settings.SettingsActivity; 44 import com.android.settings.Utils; 45 import com.android.settings.applications.InstalledAppDetails; 46 import com.android.settings.dashboard.SummaryLoader; 47 import com.android.settings.widget.SwitchBar; 48 import com.android.settingslib.RestrictedLockUtils; 49 import com.android.settingslib.RestrictedSwitchPreference; 50 import com.android.settingslib.location.RecentLocationApps; 51 52 import java.util.ArrayList; 53 import java.util.Collections; 54 import java.util.Comparator; 55 import java.util.List; 56 57 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 58 59 /** 60 * System location settings (Settings > Location). The screen has three parts: 61 * <ul> 62 * <li>Platform location controls</li> 63 * <ul> 64 * <li>In switch bar: location master switch. Used to toggle 65 * {@link android.provider.Settings.Secure#LOCATION_MODE} between 66 * {@link android.provider.Settings.Secure#LOCATION_MODE_OFF} and another location mode. 67 * </li> 68 * <li>Mode preference: only available if the master switch is on, selects between 69 * {@link android.provider.Settings.Secure#LOCATION_MODE} of 70 * {@link android.provider.Settings.Secure#LOCATION_MODE_HIGH_ACCURACY}, 71 * {@link android.provider.Settings.Secure#LOCATION_MODE_BATTERY_SAVING}, or 72 * {@link android.provider.Settings.Secure#LOCATION_MODE_SENSORS_ONLY}.</li> 73 * </ul> 74 * <li>Recent location requests: automatically populated by {@link RecentLocationApps}</li> 75 * <li>Location services: multi-app settings provided from outside the Android framework. Each 76 * is injected by a system-partition app via the {@link SettingInjectorService} API.</li> 77 * </ul> 78 * <p> 79 * Note that as of KitKat, the {@link SettingInjectorService} is the preferred method for OEMs to 80 * add their own settings to this page, rather than directly modifying the framework code. Among 81 * other things, this simplifies integration with future changes to the default (AOSP) 82 * implementation. 83 */ 84 public class LocationSettings extends LocationSettingsBase 85 implements SwitchBar.OnSwitchChangeListener { 86 87 private static final String TAG = "LocationSettings"; 88 89 /** 90 * Key for managed profile location switch preference. Shown only 91 * if there is a managed profile. 92 */ 93 private static final String KEY_MANAGED_PROFILE_SWITCH = "managed_profile_location_switch"; 94 /** Key for preference screen "Mode" */ 95 private static final String KEY_LOCATION_MODE = "location_mode"; 96 /** Key for preference category "Recent location requests" */ 97 private static final String KEY_RECENT_LOCATION_REQUESTS = "recent_location_requests"; 98 /** Key for preference category "Location services" */ 99 private static final String KEY_LOCATION_SERVICES = "location_services"; 100 101 private static final int MENU_SCANNING = Menu.FIRST; 102 103 private SwitchBar mSwitchBar; 104 private Switch mSwitch; 105 private boolean mValidListener = false; 106 private UserHandle mManagedProfile; 107 private RestrictedSwitchPreference mManagedProfileSwitch; 108 private Preference mLocationMode; 109 private PreferenceCategory mCategoryRecentLocationRequests; 110 /** Receives UPDATE_INTENT */ 111 private BroadcastReceiver mReceiver; 112 private SettingsInjector injector; 113 private UserManager mUm; 114 115 @Override getMetricsCategory()116 protected int getMetricsCategory() { 117 return MetricsEvent.LOCATION; 118 } 119 120 @Override onActivityCreated(Bundle savedInstanceState)121 public void onActivityCreated(Bundle savedInstanceState) { 122 super.onActivityCreated(savedInstanceState); 123 124 final SettingsActivity activity = (SettingsActivity) getActivity(); 125 mUm = (UserManager) activity.getSystemService(Context.USER_SERVICE); 126 127 setHasOptionsMenu(true); 128 mSwitchBar = activity.getSwitchBar(); 129 mSwitch = mSwitchBar.getSwitch(); 130 mSwitchBar.show(); 131 } 132 133 @Override onDestroyView()134 public void onDestroyView() { 135 super.onDestroyView(); 136 mSwitchBar.hide(); 137 } 138 139 @Override onResume()140 public void onResume() { 141 super.onResume(); 142 createPreferenceHierarchy(); 143 if (!mValidListener) { 144 mSwitchBar.addOnSwitchChangeListener(this); 145 mValidListener = true; 146 } 147 } 148 149 @Override onPause()150 public void onPause() { 151 try { 152 getActivity().unregisterReceiver(mReceiver); 153 } catch (RuntimeException e) { 154 // Ignore exceptions caused by race condition 155 if (Log.isLoggable(TAG, Log.VERBOSE)) { 156 Log.v(TAG, "Swallowing " + e); 157 } 158 } 159 if (mValidListener) { 160 mSwitchBar.removeOnSwitchChangeListener(this); 161 mValidListener = false; 162 } 163 super.onPause(); 164 } 165 addPreferencesSorted(List<Preference> prefs, PreferenceGroup container)166 private void addPreferencesSorted(List<Preference> prefs, PreferenceGroup container) { 167 // If there's some items to display, sort the items and add them to the container. 168 Collections.sort(prefs, new Comparator<Preference>() { 169 @Override 170 public int compare(Preference lhs, Preference rhs) { 171 return lhs.getTitle().toString().compareTo(rhs.getTitle().toString()); 172 } 173 }); 174 for (Preference entry : prefs) { 175 container.addPreference(entry); 176 } 177 } 178 createPreferenceHierarchy()179 private PreferenceScreen createPreferenceHierarchy() { 180 final SettingsActivity activity = (SettingsActivity) getActivity(); 181 PreferenceScreen root = getPreferenceScreen(); 182 if (root != null) { 183 root.removeAll(); 184 } 185 addPreferencesFromResource(R.xml.location_settings); 186 root = getPreferenceScreen(); 187 188 setupManagedProfileCategory(root); 189 mLocationMode = root.findPreference(KEY_LOCATION_MODE); 190 mLocationMode.setOnPreferenceClickListener( 191 new Preference.OnPreferenceClickListener() { 192 @Override 193 public boolean onPreferenceClick(Preference preference) { 194 activity.startPreferencePanel( 195 LocationMode.class.getName(), null, 196 R.string.location_mode_screen_title, null, LocationSettings.this, 197 0); 198 return true; 199 } 200 }); 201 202 mCategoryRecentLocationRequests = 203 (PreferenceCategory) root.findPreference(KEY_RECENT_LOCATION_REQUESTS); 204 RecentLocationApps recentApps = new RecentLocationApps(activity); 205 List<RecentLocationApps.Request> recentLocationRequests = recentApps.getAppList(); 206 List<Preference> recentLocationPrefs = new ArrayList<>(recentLocationRequests.size()); 207 for (final RecentLocationApps.Request request : recentLocationRequests) { 208 DimmableIconPreference pref = new DimmableIconPreference(getPrefContext(), 209 request.contentDescription); 210 pref.setIcon(request.icon); 211 pref.setTitle(request.label); 212 if (request.isHighBattery) { 213 pref.setSummary(R.string.location_high_battery_use); 214 } else { 215 pref.setSummary(R.string.location_low_battery_use); 216 } 217 pref.setOnPreferenceClickListener( 218 new PackageEntryClickedListener(request.packageName, request.userHandle)); 219 recentLocationPrefs.add(pref); 220 221 } 222 if (recentLocationRequests.size() > 0) { 223 addPreferencesSorted(recentLocationPrefs, mCategoryRecentLocationRequests); 224 } else { 225 // If there's no item to display, add a "No recent apps" item. 226 Preference banner = new Preference(getPrefContext()); 227 banner.setLayoutResource(R.layout.location_list_no_item); 228 banner.setTitle(R.string.location_no_recent_apps); 229 banner.setSelectable(false); 230 mCategoryRecentLocationRequests.addPreference(banner); 231 } 232 233 boolean lockdownOnLocationAccess = false; 234 // Checking if device policy has put a location access lock-down on the managed 235 // profile. If managed profile has lock-down on location access then its 236 // injected location services must not be shown. 237 if (mManagedProfile != null 238 && mUm.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile)) { 239 lockdownOnLocationAccess = true; 240 } 241 addLocationServices(activity, root, lockdownOnLocationAccess); 242 243 refreshLocationMode(); 244 return root; 245 } 246 setupManagedProfileCategory(PreferenceScreen root)247 private void setupManagedProfileCategory(PreferenceScreen root) { 248 // Looking for a managed profile. If there are no managed profiles then we are removing the 249 // managed profile category. 250 mManagedProfile = Utils.getManagedProfile(mUm); 251 if (mManagedProfile == null) { 252 // There is no managed profile 253 root.removePreference(root.findPreference(KEY_MANAGED_PROFILE_SWITCH)); 254 mManagedProfileSwitch = null; 255 } else { 256 mManagedProfileSwitch = (RestrictedSwitchPreference)root 257 .findPreference(KEY_MANAGED_PROFILE_SWITCH); 258 mManagedProfileSwitch.setOnPreferenceClickListener(null); 259 } 260 } 261 changeManagedProfileLocationAccessStatus(boolean mainSwitchOn)262 private void changeManagedProfileLocationAccessStatus(boolean mainSwitchOn) { 263 if (mManagedProfileSwitch == null) { 264 return; 265 } 266 mManagedProfileSwitch.setOnPreferenceClickListener(null); 267 final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(getActivity(), 268 UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile.getIdentifier()); 269 final boolean isRestrictedByBase = isManagedProfileRestrictedByBase(); 270 if (!isRestrictedByBase && admin != null) { 271 mManagedProfileSwitch.setDisabledByAdmin(admin); 272 mManagedProfileSwitch.setChecked(false); 273 } else { 274 boolean enabled = mainSwitchOn; 275 mManagedProfileSwitch.setEnabled(enabled); 276 277 int summaryResId = R.string.switch_off_text; 278 if (!enabled) { 279 mManagedProfileSwitch.setChecked(false); 280 } else { 281 mManagedProfileSwitch.setChecked(!isRestrictedByBase); 282 summaryResId = (isRestrictedByBase ? 283 R.string.switch_off_text : R.string.switch_on_text); 284 mManagedProfileSwitch.setOnPreferenceClickListener( 285 mManagedProfileSwitchClickListener); 286 } 287 mManagedProfileSwitch.setSummary(summaryResId); 288 } 289 } 290 291 /** 292 * Add the settings injected by external apps into the "App Settings" category. Hides the 293 * category if there are no injected settings. 294 * 295 * Reloads the settings whenever receives 296 * {@link SettingInjectorService#ACTION_INJECTED_SETTING_CHANGED}. 297 */ addLocationServices(Context context, PreferenceScreen root, boolean lockdownOnLocationAccess)298 private void addLocationServices(Context context, PreferenceScreen root, 299 boolean lockdownOnLocationAccess) { 300 PreferenceCategory categoryLocationServices = 301 (PreferenceCategory) root.findPreference(KEY_LOCATION_SERVICES); 302 injector = new SettingsInjector(context); 303 // If location access is locked down by device policy then we only show injected settings 304 // for the primary profile. 305 List<Preference> locationServices = injector.getInjectedSettings(lockdownOnLocationAccess ? 306 UserHandle.myUserId() : UserHandle.USER_CURRENT); 307 308 mReceiver = new BroadcastReceiver() { 309 @Override 310 public void onReceive(Context context, Intent intent) { 311 if (Log.isLoggable(TAG, Log.DEBUG)) { 312 Log.d(TAG, "Received settings change intent: " + intent); 313 } 314 injector.reloadStatusMessages(); 315 } 316 }; 317 318 IntentFilter filter = new IntentFilter(); 319 filter.addAction(SettingInjectorService.ACTION_INJECTED_SETTING_CHANGED); 320 context.registerReceiver(mReceiver, filter); 321 322 if (locationServices.size() > 0) { 323 addPreferencesSorted(locationServices, categoryLocationServices); 324 } else { 325 // If there's no item to display, remove the whole category. 326 root.removePreference(categoryLocationServices); 327 } 328 } 329 330 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)331 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 332 menu.add(0, MENU_SCANNING, 0, R.string.location_menu_scanning); 333 // The super class adds "Help & Feedback" menu item. 334 super.onCreateOptionsMenu(menu, inflater); 335 } 336 337 @Override onOptionsItemSelected(MenuItem item)338 public boolean onOptionsItemSelected(MenuItem item) { 339 final SettingsActivity activity = (SettingsActivity) getActivity(); 340 switch (item.getItemId()) { 341 case MENU_SCANNING: 342 activity.startPreferencePanel( 343 ScanningSettings.class.getName(), null, 344 R.string.location_scanning_screen_title, null, LocationSettings.this, 345 0); 346 return true; 347 default: 348 return super.onOptionsItemSelected(item); 349 } 350 } 351 352 @Override getHelpResource()353 public int getHelpResource() { 354 return R.string.help_url_location_access; 355 } 356 getLocationString(int mode)357 private static int getLocationString(int mode) { 358 switch (mode) { 359 case android.provider.Settings.Secure.LOCATION_MODE_OFF: 360 return R.string.location_mode_location_off_title; 361 case android.provider.Settings.Secure.LOCATION_MODE_SENSORS_ONLY: 362 return R.string.location_mode_sensors_only_title; 363 case android.provider.Settings.Secure.LOCATION_MODE_BATTERY_SAVING: 364 return R.string.location_mode_battery_saving_title; 365 case android.provider.Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: 366 return R.string.location_mode_high_accuracy_title; 367 } 368 return 0; 369 } 370 371 @Override onModeChanged(int mode, boolean restricted)372 public void onModeChanged(int mode, boolean restricted) { 373 int modeDescription = getLocationString(mode); 374 if (modeDescription != 0) { 375 mLocationMode.setSummary(modeDescription); 376 } 377 378 // Restricted user can't change the location mode, so disable the master switch. But in some 379 // corner cases, the location might still be enabled. In such case the master switch should 380 // be disabled but checked. 381 final boolean enabled = (mode != android.provider.Settings.Secure.LOCATION_MODE_OFF); 382 EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(getActivity(), 383 UserManager.DISALLOW_SHARE_LOCATION, UserHandle.myUserId()); 384 boolean hasBaseUserRestriction = RestrictedLockUtils.hasBaseUserRestriction(getActivity(), 385 UserManager.DISALLOW_SHARE_LOCATION, UserHandle.myUserId()); 386 // Disable the whole switch bar instead of the switch itself. If we disabled the switch 387 // only, it would be re-enabled again if the switch bar is not disabled. 388 if (!hasBaseUserRestriction && admin != null) { 389 mSwitchBar.setDisabledByAdmin(admin); 390 } else { 391 mSwitchBar.setEnabled(!restricted); 392 } 393 mLocationMode.setEnabled(enabled && !restricted); 394 mCategoryRecentLocationRequests.setEnabled(enabled); 395 396 if (enabled != mSwitch.isChecked()) { 397 // set listener to null so that that code below doesn't trigger onCheckedChanged() 398 if (mValidListener) { 399 mSwitchBar.removeOnSwitchChangeListener(this); 400 } 401 mSwitch.setChecked(enabled); 402 if (mValidListener) { 403 mSwitchBar.addOnSwitchChangeListener(this); 404 } 405 } 406 407 changeManagedProfileLocationAccessStatus(enabled); 408 409 // As a safety measure, also reloads on location mode change to ensure the settings are 410 // up-to-date even if an affected app doesn't send the setting changed broadcast. 411 injector.reloadStatusMessages(); 412 } 413 414 /** 415 * Listens to the state change of the location master switch. 416 */ 417 @Override onSwitchChanged(Switch switchView, boolean isChecked)418 public void onSwitchChanged(Switch switchView, boolean isChecked) { 419 if (isChecked) { 420 setLocationMode(android.provider.Settings.Secure.LOCATION_MODE_PREVIOUS); 421 } else { 422 setLocationMode(android.provider.Settings.Secure.LOCATION_MODE_OFF); 423 } 424 } 425 isManagedProfileRestrictedByBase()426 private boolean isManagedProfileRestrictedByBase() { 427 if (mManagedProfile == null) { 428 return false; 429 } 430 return mUm.hasBaseUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile); 431 } 432 433 private Preference.OnPreferenceClickListener mManagedProfileSwitchClickListener = 434 new Preference.OnPreferenceClickListener() { 435 @Override 436 public boolean onPreferenceClick(Preference preference) { 437 final boolean switchState = mManagedProfileSwitch.isChecked(); 438 mUm.setUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, 439 !switchState, mManagedProfile); 440 mManagedProfileSwitch.setSummary(switchState ? 441 R.string.switch_on_text : R.string.switch_off_text); 442 return true; 443 } 444 }; 445 446 private class PackageEntryClickedListener 447 implements Preference.OnPreferenceClickListener { 448 private String mPackage; 449 private UserHandle mUserHandle; 450 PackageEntryClickedListener(String packageName, UserHandle userHandle)451 public PackageEntryClickedListener(String packageName, UserHandle userHandle) { 452 mPackage = packageName; 453 mUserHandle = userHandle; 454 } 455 456 @Override onPreferenceClick(Preference preference)457 public boolean onPreferenceClick(Preference preference) { 458 // start new fragment to display extended information 459 Bundle args = new Bundle(); 460 args.putString(InstalledAppDetails.ARG_PACKAGE_NAME, mPackage); 461 ((SettingsActivity) getActivity()).startPreferencePanelAsUser( 462 InstalledAppDetails.class.getName(), args, 463 R.string.application_info_label, null, mUserHandle); 464 return true; 465 } 466 } 467 468 private static class SummaryProvider implements SummaryLoader.SummaryProvider { 469 470 private final Context mContext; 471 private final SummaryLoader mSummaryLoader; 472 SummaryProvider(Context context, SummaryLoader summaryLoader)473 public SummaryProvider(Context context, SummaryLoader summaryLoader) { 474 mContext = context; 475 mSummaryLoader = summaryLoader; 476 } 477 478 @Override setListening(boolean listening)479 public void setListening(boolean listening) { 480 if (listening) { 481 int mode = Settings.Secure.getInt(mContext.getContentResolver(), 482 Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF); 483 if (mode != Settings.Secure.LOCATION_MODE_OFF) { 484 mSummaryLoader.setSummary(this, mContext.getString(R.string.location_on_summary, 485 mContext.getString(getLocationString(mode)))); 486 } else { 487 mSummaryLoader.setSummary(this, 488 mContext.getString(R.string.location_off_summary)); 489 } 490 } 491 } 492 } 493 494 public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY 495 = new SummaryLoader.SummaryProviderFactory() { 496 @Override 497 public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, 498 SummaryLoader summaryLoader) { 499 return new SummaryProvider(activity, summaryLoader); 500 } 501 }; 502 } 503