Home | History | Annotate | Download | only in location
      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
    116     protected int getMetricsCategory() {
    117         return MetricsEvent.LOCATION;
    118     }
    119 
    120     @Override
    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
    134     public void onDestroyView() {
    135         super.onDestroyView();
    136         mSwitchBar.hide();
    137     }
    138 
    139     @Override
    140     public void onResume() {
    141         super.onResume();
    142         createPreferenceHierarchy();
    143         if (!mValidListener) {
    144             mSwitchBar.addOnSwitchChangeListener(this);
    145             mValidListener = true;
    146         }
    147     }
    148 
    149     @Override
    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 
    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 
    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 
    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 
    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      */
    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
    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
    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
    353     public int getHelpResource() {
    354         return R.string.help_url_location_access;
    355     }
    356 
    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
    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
    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 
    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 
    451         public PackageEntryClickedListener(String packageName, UserHandle userHandle) {
    452             mPackage = packageName;
    453             mUserHandle = userHandle;
    454         }
    455 
    456         @Override
    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 
    473         public SummaryProvider(Context context, SummaryLoader summaryLoader) {
    474             mContext = context;
    475             mSummaryLoader = summaryLoader;
    476         }
    477 
    478         @Override
    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