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.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.location.SettingInjectorService; 24 import android.os.Bundle; 25 import android.os.UserHandle; 26 import android.os.UserManager; 27 import android.preference.Preference; 28 import android.preference.PreferenceCategory; 29 import android.preference.PreferenceGroup; 30 import android.preference.PreferenceScreen; 31 import android.util.Log; 32 import android.view.Menu; 33 import android.view.MenuInflater; 34 import android.view.MenuItem; 35 import android.widget.Switch; 36 import com.android.internal.logging.MetricsLogger; 37 import com.android.settings.R; 38 import com.android.settings.SettingsActivity; 39 import com.android.settings.Utils; 40 import com.android.settings.widget.SwitchBar; 41 42 import java.util.Collections; 43 import java.util.Comparator; 44 import java.util.List; 45 46 /** 47 * System location settings (Settings > Location). The screen has three parts: 48 * <ul> 49 * <li>Platform location controls</li> 50 * <ul> 51 * <li>In switch bar: location master switch. Used to toggle 52 * {@link android.provider.Settings.Secure#LOCATION_MODE} between 53 * {@link android.provider.Settings.Secure#LOCATION_MODE_OFF} and another location mode. 54 * </li> 55 * <li>Mode preference: only available if the master switch is on, selects between 56 * {@link android.provider.Settings.Secure#LOCATION_MODE} of 57 * {@link android.provider.Settings.Secure#LOCATION_MODE_HIGH_ACCURACY}, 58 * {@link android.provider.Settings.Secure#LOCATION_MODE_BATTERY_SAVING}, or 59 * {@link android.provider.Settings.Secure#LOCATION_MODE_SENSORS_ONLY}.</li> 60 * </ul> 61 * <li>Recent location requests: automatically populated by {@link RecentLocationApps}</li> 62 * <li>Location services: multi-app settings provided from outside the Android framework. Each 63 * is injected by a system-partition app via the {@link SettingInjectorService} API.</li> 64 * </ul> 65 * <p> 66 * Note that as of KitKat, the {@link SettingInjectorService} is the preferred method for OEMs to 67 * add their own settings to this page, rather than directly modifying the framework code. Among 68 * other things, this simplifies integration with future changes to the default (AOSP) 69 * implementation. 70 */ 71 public class LocationSettings extends LocationSettingsBase 72 implements SwitchBar.OnSwitchChangeListener { 73 74 private static final String TAG = "LocationSettings"; 75 76 /** 77 * Key for managed profile location preference category. Category is shown only 78 * if there is a managed profile 79 */ 80 private static final String KEY_MANAGED_PROFILE_CATEGORY = "managed_profile_location_category"; 81 /** 82 * Key for managed profile location preference. Note it used to be a switch pref and we had to 83 * keep the key as strings had been submitted for string freeze before the decision to 84 * demote this to a simple preference was made. TODO: Candidate for refactoring. 85 */ 86 private static final String KEY_MANAGED_PROFILE_PREFERENCE = "managed_profile_location_switch"; 87 /** Key for preference screen "Mode" */ 88 private static final String KEY_LOCATION_MODE = "location_mode"; 89 /** Key for preference category "Recent location requests" */ 90 private static final String KEY_RECENT_LOCATION_REQUESTS = "recent_location_requests"; 91 /** Key for preference category "Location services" */ 92 private static final String KEY_LOCATION_SERVICES = "location_services"; 93 94 private static final int MENU_SCANNING = Menu.FIRST; 95 96 private SwitchBar mSwitchBar; 97 private Switch mSwitch; 98 private boolean mValidListener = false; 99 private UserHandle mManagedProfile; 100 private Preference mManagedProfilePreference; 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 protected int getMetricsCategory() { 110 return MetricsLogger.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 mSwitchBar = activity.getSwitchBar(); 121 mSwitch = mSwitchBar.getSwitch(); 122 mSwitchBar.show(); 123 } 124 125 @Override onDestroyView()126 public void onDestroyView() { 127 super.onDestroyView(); 128 mSwitchBar.hide(); 129 } 130 131 @Override onResume()132 public void onResume() { 133 super.onResume(); 134 createPreferenceHierarchy(); 135 if (!mValidListener) { 136 mSwitchBar.addOnSwitchChangeListener(this); 137 mValidListener = true; 138 } 139 } 140 141 @Override onPause()142 public void onPause() { 143 try { 144 getActivity().unregisterReceiver(mReceiver); 145 } catch (RuntimeException e) { 146 // Ignore exceptions caused by race condition 147 if (Log.isLoggable(TAG, Log.VERBOSE)) { 148 Log.v(TAG, "Swallowing " + e); 149 } 150 } 151 if (mValidListener) { 152 mSwitchBar.removeOnSwitchChangeListener(this); 153 mValidListener = false; 154 } 155 super.onPause(); 156 } 157 addPreferencesSorted(List<Preference> prefs, PreferenceGroup container)158 private void addPreferencesSorted(List<Preference> prefs, PreferenceGroup container) { 159 // If there's some items to display, sort the items and add them to the container. 160 Collections.sort(prefs, new Comparator<Preference>() { 161 @Override 162 public int compare(Preference lhs, Preference rhs) { 163 return lhs.getTitle().toString().compareTo(rhs.getTitle().toString()); 164 } 165 }); 166 for (Preference entry : prefs) { 167 container.addPreference(entry); 168 } 169 } 170 createPreferenceHierarchy()171 private PreferenceScreen createPreferenceHierarchy() { 172 final SettingsActivity activity = (SettingsActivity) getActivity(); 173 PreferenceScreen root = getPreferenceScreen(); 174 if (root != null) { 175 root.removeAll(); 176 } 177 addPreferencesFromResource(R.xml.location_settings); 178 root = getPreferenceScreen(); 179 180 setupManagedProfileCategory(root); 181 mLocationMode = root.findPreference(KEY_LOCATION_MODE); 182 mLocationMode.setOnPreferenceClickListener( 183 new Preference.OnPreferenceClickListener() { 184 @Override 185 public boolean onPreferenceClick(Preference preference) { 186 activity.startPreferencePanel( 187 LocationMode.class.getName(), null, 188 R.string.location_mode_screen_title, null, LocationSettings.this, 189 0); 190 return true; 191 } 192 }); 193 194 mCategoryRecentLocationRequests = 195 (PreferenceCategory) root.findPreference(KEY_RECENT_LOCATION_REQUESTS); 196 RecentLocationApps recentApps = new RecentLocationApps(activity); 197 List<Preference> recentLocationRequests = recentApps.getAppList(); 198 if (recentLocationRequests.size() > 0) { 199 addPreferencesSorted(recentLocationRequests, mCategoryRecentLocationRequests); 200 } else { 201 // If there's no item to display, add a "No recent apps" item. 202 Preference banner = new Preference(activity); 203 banner.setLayoutResource(R.layout.location_list_no_item); 204 banner.setTitle(R.string.location_no_recent_apps); 205 banner.setSelectable(false); 206 mCategoryRecentLocationRequests.addPreference(banner); 207 } 208 209 boolean lockdownOnLocationAccess = false; 210 // Checking if device policy has put a location access lock-down on the managed 211 // profile. If managed profile has lock-down on location access then its 212 // injected location services must not be shown. 213 if (mManagedProfile != null 214 && mUm.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile)) { 215 lockdownOnLocationAccess = true; 216 } 217 addLocationServices(activity, root, lockdownOnLocationAccess); 218 219 refreshLocationMode(); 220 return root; 221 } 222 setupManagedProfileCategory(PreferenceScreen root)223 private void setupManagedProfileCategory(PreferenceScreen root) { 224 // Looking for a managed profile. If there are no managed profiles then we are removing the 225 // managed profile category. 226 mManagedProfile = Utils.getManagedProfile(mUm); 227 if (mManagedProfile == null) { 228 // There is no managed profile 229 root.removePreference(root.findPreference(KEY_MANAGED_PROFILE_CATEGORY)); 230 mManagedProfilePreference = null; 231 } else { 232 mManagedProfilePreference = root.findPreference(KEY_MANAGED_PROFILE_PREFERENCE); 233 mManagedProfilePreference.setOnPreferenceClickListener(null); 234 } 235 } 236 changeManagedProfileLocationAccessStatus(boolean enabled, int summaryResId)237 private void changeManagedProfileLocationAccessStatus(boolean enabled, int summaryResId) { 238 if (mManagedProfilePreference == null) { 239 return; 240 } 241 mManagedProfilePreference.setEnabled(enabled); 242 mManagedProfilePreference.setSummary(summaryResId); 243 } 244 245 /** 246 * Add the settings injected by external apps into the "App Settings" category. Hides the 247 * category if there are no injected settings. 248 * 249 * Reloads the settings whenever receives 250 * {@link SettingInjectorService#ACTION_INJECTED_SETTING_CHANGED}. 251 */ addLocationServices(Context context, PreferenceScreen root, boolean lockdownOnLocationAccess)252 private void addLocationServices(Context context, PreferenceScreen root, 253 boolean lockdownOnLocationAccess) { 254 PreferenceCategory categoryLocationServices = 255 (PreferenceCategory) root.findPreference(KEY_LOCATION_SERVICES); 256 injector = new SettingsInjector(context); 257 // If location access is locked down by device policy then we only show injected settings 258 // for the primary profile. 259 List<Preference> locationServices = injector.getInjectedSettings(lockdownOnLocationAccess ? 260 UserHandle.myUserId() : UserHandle.USER_CURRENT); 261 262 mReceiver = new BroadcastReceiver() { 263 @Override 264 public void onReceive(Context context, Intent intent) { 265 if (Log.isLoggable(TAG, Log.DEBUG)) { 266 Log.d(TAG, "Received settings change intent: " + intent); 267 } 268 injector.reloadStatusMessages(); 269 } 270 }; 271 272 IntentFilter filter = new IntentFilter(); 273 filter.addAction(SettingInjectorService.ACTION_INJECTED_SETTING_CHANGED); 274 context.registerReceiver(mReceiver, filter); 275 276 if (locationServices.size() > 0) { 277 addPreferencesSorted(locationServices, categoryLocationServices); 278 } else { 279 // If there's no item to display, remove the whole category. 280 root.removePreference(categoryLocationServices); 281 } 282 } 283 284 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)285 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 286 menu.add(0, MENU_SCANNING, 0, R.string.location_menu_scanning); 287 // The super class adds "Help & Feedback" menu item. 288 super.onCreateOptionsMenu(menu, inflater); 289 } 290 291 @Override onOptionsItemSelected(MenuItem item)292 public boolean onOptionsItemSelected(MenuItem item) { 293 final SettingsActivity activity = (SettingsActivity) getActivity(); 294 switch (item.getItemId()) { 295 case MENU_SCANNING: 296 activity.startPreferencePanel( 297 ScanningSettings.class.getName(), null, 298 R.string.location_scanning_screen_title, null, LocationSettings.this, 299 0); 300 return true; 301 default: 302 return super.onOptionsItemSelected(item); 303 } 304 } 305 306 @Override getHelpResource()307 public int getHelpResource() { 308 return R.string.help_url_location_access; 309 } 310 311 @Override onModeChanged(int mode, boolean restricted)312 public void onModeChanged(int mode, boolean restricted) { 313 switch (mode) { 314 case android.provider.Settings.Secure.LOCATION_MODE_OFF: 315 mLocationMode.setSummary(R.string.location_mode_location_off_title); 316 break; 317 case android.provider.Settings.Secure.LOCATION_MODE_SENSORS_ONLY: 318 mLocationMode.setSummary(R.string.location_mode_sensors_only_title); 319 break; 320 case android.provider.Settings.Secure.LOCATION_MODE_BATTERY_SAVING: 321 mLocationMode.setSummary(R.string.location_mode_battery_saving_title); 322 break; 323 case android.provider.Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: 324 mLocationMode.setSummary(R.string.location_mode_high_accuracy_title); 325 break; 326 default: 327 break; 328 } 329 330 // Restricted user can't change the location mode, so disable the master switch. But in some 331 // corner cases, the location might still be enabled. In such case the master switch should 332 // be disabled but checked. 333 final boolean enabled = (mode != android.provider.Settings.Secure.LOCATION_MODE_OFF); 334 // Disable the whole switch bar instead of the switch itself. If we disabled the switch 335 // only, it would be re-enabled again if the switch bar is not disabled. 336 mSwitchBar.setEnabled(!restricted); 337 mLocationMode.setEnabled(enabled && !restricted); 338 mCategoryRecentLocationRequests.setEnabled(enabled); 339 340 if (enabled != mSwitch.isChecked()) { 341 // set listener to null so that that code below doesn't trigger onCheckedChanged() 342 if (mValidListener) { 343 mSwitchBar.removeOnSwitchChangeListener(this); 344 } 345 mSwitch.setChecked(enabled); 346 if (mValidListener) { 347 mSwitchBar.addOnSwitchChangeListener(this); 348 } 349 } 350 351 if (mManagedProfilePreference != null) { 352 if (mUm.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION, mManagedProfile)) { 353 changeManagedProfileLocationAccessStatus(false, 354 R.string.managed_profile_location_switch_lockdown); 355 } else { 356 if (enabled) { 357 changeManagedProfileLocationAccessStatus(true, R.string.switch_on_text); 358 } else { 359 changeManagedProfileLocationAccessStatus(false, R.string.switch_off_text); 360 } 361 } 362 } 363 364 // As a safety measure, also reloads on location mode change to ensure the settings are 365 // up-to-date even if an affected app doesn't send the setting changed broadcast. 366 injector.reloadStatusMessages(); 367 } 368 369 /** 370 * Listens to the state change of the location master switch. 371 */ 372 @Override onSwitchChanged(Switch switchView, boolean isChecked)373 public void onSwitchChanged(Switch switchView, boolean isChecked) { 374 if (isChecked) { 375 setLocationMode(android.provider.Settings.Secure.LOCATION_MODE_HIGH_ACCURACY); 376 } else { 377 setLocationMode(android.provider.Settings.Secure.LOCATION_MODE_OFF); 378 } 379 } 380 } 381