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             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 
setupManagedProfileCategory(PreferenceScreen root)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 
changeManagedProfileLocationAccessStatus(boolean mainSwitchOn)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      */
addLocationServices(Context context, PreferenceScreen root, boolean lockdownOnLocationAccess)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
getHelpResource()334     public int getHelpResource() {
335         return R.string.help_url_location_access;
336     }
337 
338     @Override
onModeChanged(int mode, boolean restricted)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
onSwitchChanged(Switch switchView, boolean isChecked)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 
isManagedProfileRestrictedByBase()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 
PackageEntryClickedListener(String packageName, UserHandle userHandle)418         public PackageEntryClickedListener(String packageName, UserHandle userHandle) {
419             mPackage = packageName;
420             mUserHandle = userHandle;
421         }
422 
423         @Override
onPreferenceClick(Preference preference)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 
SummaryProvider(Context context, SummaryLoader summaryLoader)441         public SummaryProvider(Context context, SummaryLoader summaryLoader) {
442             mContext = context;
443             mSummaryLoader = summaryLoader;
444         }
445 
446         @Override
setListening(boolean listening)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