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 getMetricsCategory()109 public int getMetricsCategory() { 110 return MetricsEvent.LOCATION; 111 } 112 113 @Override onActivityCreated(Bundle savedInstanceState)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 onDestroyView()129 public void onDestroyView() { 130 super.onDestroyView(); 131 mSwitchBar.hide(); 132 } 133 134 @Override onResume()135 public void onResume() { 136 super.onResume(); 137 createPreferenceHierarchy(); 138 if (!mValidListener) { 139 mSwitchBar.addOnSwitchChangeListener(this); 140 mValidListener = true; 141 } 142 } 143 144 @Override onPause()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 addPreferencesSorted(List<Preference> prefs, PreferenceGroup container)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 createPreferenceHierarchy()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 pref.setOnPreferenceClickListener( 215 new PackageEntryClickedListener(request.packageName, request.userHandle)); 216 recentLocationPrefs.add(pref); 217 218 } 219 if (recentLocationRequests.size() > 0) { 220 addPreferencesSorted(recentLocationPrefs, mCategoryRecentLocationRequests); 221 } else { 222 // If there's no item to display, add a "No recent apps" item. 223 Preference banner = new Preference(getPrefContext()); 224 banner.setLayoutResource(R.layout.location_list_no_item); 225 banner.setTitle(R.string.location_no_recent_apps); 226 banner.setSelectable(false); 227 mCategoryRecentLocationRequests.addPreference(banner); 228 } 229 230 boolean lockdownOnLocationAccess = false; 231 // Checking if device policy has put a location access lock-down on the managed 232 // profile. If managed profile has lock-down on location access then its 233 // injected location services must not be shown. 234 if (mManagedProfile != null 235 && mUm.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile)) { 236 lockdownOnLocationAccess = true; 237 } 238 addLocationServices(activity, root, lockdownOnLocationAccess); 239 240 refreshLocationMode(); 241 return root; 242 } 243 setupManagedProfileCategory(PreferenceScreen root)244 private void setupManagedProfileCategory(PreferenceScreen root) { 245 // Looking for a managed profile. If there are no managed profiles then we are removing the 246 // managed profile category. 247 mManagedProfile = Utils.getManagedProfile(mUm); 248 if (mManagedProfile == null) { 249 // There is no managed profile 250 root.removePreference(root.findPreference(KEY_MANAGED_PROFILE_SWITCH)); 251 mManagedProfileSwitch = null; 252 } else { 253 mManagedProfileSwitch = (RestrictedSwitchPreference)root 254 .findPreference(KEY_MANAGED_PROFILE_SWITCH); 255 mManagedProfileSwitch.setOnPreferenceClickListener(null); 256 } 257 } 258 changeManagedProfileLocationAccessStatus(boolean mainSwitchOn)259 private void changeManagedProfileLocationAccessStatus(boolean mainSwitchOn) { 260 if (mManagedProfileSwitch == null) { 261 return; 262 } 263 mManagedProfileSwitch.setOnPreferenceClickListener(null); 264 final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(getActivity(), 265 UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile.getIdentifier()); 266 final boolean isRestrictedByBase = isManagedProfileRestrictedByBase(); 267 if (!isRestrictedByBase && admin != null) { 268 mManagedProfileSwitch.setDisabledByAdmin(admin); 269 mManagedProfileSwitch.setChecked(false); 270 } else { 271 boolean enabled = mainSwitchOn; 272 mManagedProfileSwitch.setEnabled(enabled); 273 274 int summaryResId = R.string.switch_off_text; 275 if (!enabled) { 276 mManagedProfileSwitch.setChecked(false); 277 } else { 278 mManagedProfileSwitch.setChecked(!isRestrictedByBase); 279 summaryResId = (isRestrictedByBase ? 280 R.string.switch_off_text : R.string.switch_on_text); 281 mManagedProfileSwitch.setOnPreferenceClickListener( 282 mManagedProfileSwitchClickListener); 283 } 284 mManagedProfileSwitch.setSummary(summaryResId); 285 } 286 } 287 288 /** 289 * Add the settings injected by external apps into the "App Settings" category. Hides the 290 * category if there are no injected settings. 291 * 292 * Reloads the settings whenever receives 293 * {@link SettingInjectorService#ACTION_INJECTED_SETTING_CHANGED}. 294 */ addLocationServices(Context context, PreferenceScreen root, boolean lockdownOnLocationAccess)295 private void addLocationServices(Context context, PreferenceScreen root, 296 boolean lockdownOnLocationAccess) { 297 PreferenceCategory categoryLocationServices = 298 (PreferenceCategory) root.findPreference(KEY_LOCATION_SERVICES); 299 injector = new SettingsInjector(context); 300 // If location access is locked down by device policy then we only show injected settings 301 // for the primary profile. 302 final Context prefContext = categoryLocationServices.getContext(); 303 final List<Preference> locationServices = injector.getInjectedSettings(prefContext, 304 lockdownOnLocationAccess ? UserHandle.myUserId() : UserHandle.USER_CURRENT); 305 306 mReceiver = new BroadcastReceiver() { 307 @Override 308 public void onReceive(Context context, Intent intent) { 309 if (Log.isLoggable(TAG, Log.DEBUG)) { 310 Log.d(TAG, "Received settings change intent: " + intent); 311 } 312 injector.reloadStatusMessages(); 313 } 314 }; 315 316 IntentFilter filter = new IntentFilter(); 317 filter.addAction(SettingInjectorService.ACTION_INJECTED_SETTING_CHANGED); 318 context.registerReceiver(mReceiver, filter); 319 320 if (locationServices.size() > 0) { 321 addPreferencesSorted(locationServices, categoryLocationServices); 322 } else { 323 // If there's no item to display, remove the whole category. 324 root.removePreference(categoryLocationServices); 325 } 326 } 327 328 @Override getHelpResource()329 public int getHelpResource() { 330 return R.string.help_url_location_access; 331 } 332 333 @Override onModeChanged(int mode, boolean restricted)334 public void onModeChanged(int mode, boolean restricted) { 335 int modeDescription = LocationPreferenceController.getLocationString(mode); 336 if (modeDescription != 0) { 337 mLocationMode.setSummary(modeDescription); 338 } 339 340 // Restricted user can't change the location mode, so disable the master switch. But in some 341 // corner cases, the location might still be enabled. In such case the master switch should 342 // be disabled but checked. 343 final boolean enabled = (mode != android.provider.Settings.Secure.LOCATION_MODE_OFF); 344 EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(getActivity(), 345 UserManager.DISALLOW_SHARE_LOCATION, UserHandle.myUserId()); 346 boolean hasBaseUserRestriction = RestrictedLockUtils.hasBaseUserRestriction(getActivity(), 347 UserManager.DISALLOW_SHARE_LOCATION, UserHandle.myUserId()); 348 // Disable the whole switch bar instead of the switch itself. If we disabled the switch 349 // only, it would be re-enabled again if the switch bar is not disabled. 350 if (!hasBaseUserRestriction && admin != null) { 351 mSwitchBar.setDisabledByAdmin(admin); 352 } else { 353 mSwitchBar.setEnabled(!restricted); 354 } 355 mLocationMode.setEnabled(enabled && !restricted); 356 mCategoryRecentLocationRequests.setEnabled(enabled); 357 358 if (enabled != mSwitch.isChecked()) { 359 // set listener to null so that that code below doesn't trigger onCheckedChanged() 360 if (mValidListener) { 361 mSwitchBar.removeOnSwitchChangeListener(this); 362 } 363 mSwitch.setChecked(enabled); 364 if (mValidListener) { 365 mSwitchBar.addOnSwitchChangeListener(this); 366 } 367 } 368 369 changeManagedProfileLocationAccessStatus(enabled); 370 371 // As a safety measure, also reloads on location mode change to ensure the settings are 372 // up-to-date even if an affected app doesn't send the setting changed broadcast. 373 injector.reloadStatusMessages(); 374 } 375 376 /** 377 * Listens to the state change of the location master switch. 378 */ 379 @Override onSwitchChanged(Switch switchView, boolean isChecked)380 public void onSwitchChanged(Switch switchView, boolean isChecked) { 381 if (isChecked) { 382 setLocationMode(android.provider.Settings.Secure.LOCATION_MODE_PREVIOUS); 383 } else { 384 setLocationMode(android.provider.Settings.Secure.LOCATION_MODE_OFF); 385 } 386 } 387 isManagedProfileRestrictedByBase()388 private boolean isManagedProfileRestrictedByBase() { 389 if (mManagedProfile == null) { 390 return false; 391 } 392 return mUm.hasBaseUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile); 393 } 394 395 private Preference.OnPreferenceClickListener mManagedProfileSwitchClickListener = 396 new Preference.OnPreferenceClickListener() { 397 @Override 398 public boolean onPreferenceClick(Preference preference) { 399 final boolean switchState = mManagedProfileSwitch.isChecked(); 400 mUm.setUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, 401 !switchState, mManagedProfile); 402 mManagedProfileSwitch.setSummary(switchState ? 403 R.string.switch_on_text : R.string.switch_off_text); 404 return true; 405 } 406 }; 407 408 private class PackageEntryClickedListener 409 implements Preference.OnPreferenceClickListener { 410 private String mPackage; 411 private UserHandle mUserHandle; 412 PackageEntryClickedListener(String packageName, UserHandle userHandle)413 public PackageEntryClickedListener(String packageName, UserHandle userHandle) { 414 mPackage = packageName; 415 mUserHandle = userHandle; 416 } 417 418 @Override onPreferenceClick(Preference preference)419 public boolean onPreferenceClick(Preference preference) { 420 // start new fragment to display extended information 421 Bundle args = new Bundle(); 422 args.putString(InstalledAppDetails.ARG_PACKAGE_NAME, mPackage); 423 ((SettingsActivity) getActivity()).startPreferencePanelAsUser( 424 LocationSettings.this, 425 InstalledAppDetails.class.getName(), args, 426 R.string.application_info_label, null, mUserHandle); 427 return true; 428 } 429 } 430 431 private static class SummaryProvider implements SummaryLoader.SummaryProvider { 432 433 private final Context mContext; 434 private final SummaryLoader mSummaryLoader; 435 SummaryProvider(Context context, SummaryLoader summaryLoader)436 public SummaryProvider(Context context, SummaryLoader summaryLoader) { 437 mContext = context; 438 mSummaryLoader = summaryLoader; 439 } 440 441 @Override setListening(boolean listening)442 public void setListening(boolean listening) { 443 if (listening) { 444 mSummaryLoader.setSummary( 445 this, LocationPreferenceController.getLocationSummary(mContext)); 446 } 447 } 448 } 449 450 public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY 451 = new SummaryLoader.SummaryProviderFactory() { 452 @Override 453 public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, 454 SummaryLoader summaryLoader) { 455 return new SummaryProvider(activity, summaryLoader); 456 } 457 }; 458 } 459