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
17package com.android.settings.location;
18
19import android.app.Activity;
20import android.app.admin.DevicePolicyManager;
21import android.content.BroadcastReceiver;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.location.SettingInjectorService;
27import android.os.Bundle;
28import android.os.UserHandle;
29import android.os.UserManager;
30import android.provider.Settings;
31import android.support.v7.preference.Preference;
32import android.support.v7.preference.PreferenceCategory;
33import android.support.v7.preference.PreferenceGroup;
34import android.support.v7.preference.PreferenceScreen;
35import android.util.Log;
36import android.view.Menu;
37import android.view.MenuInflater;
38import android.view.MenuItem;
39import android.widget.Switch;
40import com.android.internal.logging.MetricsProto.MetricsEvent;
41import com.android.settings.DimmableIconPreference;
42import com.android.settings.R;
43import com.android.settings.SettingsActivity;
44import com.android.settings.Utils;
45import com.android.settings.applications.InstalledAppDetails;
46import com.android.settings.dashboard.SummaryLoader;
47import com.android.settings.widget.SwitchBar;
48import com.android.settingslib.RestrictedLockUtils;
49import com.android.settingslib.RestrictedSwitchPreference;
50import com.android.settingslib.location.RecentLocationApps;
51
52import java.util.ArrayList;
53import java.util.Collections;
54import java.util.Comparator;
55import java.util.List;
56
57import 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 */
84public 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