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