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