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