Merge changes from topic "location settings"

* changes:
  Display app stats for location permission
  Display recent location access in the widget
This commit is contained in:
Lifu Tang
2018-12-18 08:43:31 +00:00
committed by Android (Google) Code Review
16 changed files with 343 additions and 768 deletions

View File

@@ -827,8 +827,15 @@
<string name="location_settings_title">Location</string> <string name="location_settings_title">Location</string>
<!-- Used in the location settings to control turning on/off the feature entirely --> <!-- Used in the location settings to control turning on/off the feature entirely -->
<string name="location_settings_master_switch_title">Use location</string> <string name="location_settings_master_switch_title">Use location</string>
<!-- Summary for Location settings, explaining a few important settings under it [CHAR LIMIT=NONE]--> <!-- Summary for Location settings when location is off [CHAR LIMIT=NONE] -->
<string name="location_settings_summary">Scanning, location history</string> <string name="location_settings_summary_location_off">Off</string>
<!-- Summary for Location settings when location is on, explaining how many apps have location permission [CHAR LIMIT=NONE]-->
<plurals name="location_settings_summary_location_on">
<item quantity="one">On - <xliff:g id="count">%1$d</xliff:g> app can access location</item>
<item quantity="other">On - <xliff:g id="count">%1$d</xliff:g> apps can access location</item>
</plurals>
<!-- Location settings, loading the number of apps which have location permission [CHAR LIMIT=30] -->
<string name="location_settings_loading_app_permission_stats">Loading\u2026</string>
<!-- Main Settings screen setting option title for the item to take you to the accounts screen [CHAR LIMIT=22] --> <!-- Main Settings screen setting option title for the item to take you to the accounts screen [CHAR LIMIT=22] -->
<string name="account_settings_title">Accounts</string> <string name="account_settings_title">Accounts</string>
@@ -3630,15 +3637,28 @@
<string name="managed_profile_location_switch_title">Location for work profile</string> <string name="managed_profile_location_switch_title">Location for work profile</string>
<!-- [CHAR LIMIT=30] Location settings screen. It's a link that directs the user to a page that <!-- [CHAR LIMIT=30] Location settings screen. It's a link that directs the user to a page that
shows the location permission setting for each installed app --> shows the location permission setting for each installed app -->
<string name="location_app_level_permissions">App-level permissions</string> <string name="location_app_level_permissions">App permission</string>
<!-- [CHAR LIMIT=42] Location settings screen, sub category for recent location requests --> <!-- Summary for app permission on Location settings page when location is off [CHAR LIMIT=NONE] -->
<string name="location_category_recent_location_requests">Recent location requests</string> <string name="location_app_permission_summary_location_off">Location is off</string>
<!-- Location settings screen, displayed when there're more than three recent location requests --> <!-- Summary for Location settings when location is on, explaining how many apps have location permission [CHAR LIMIT=NONE]-->
<string name="location_recent_location_requests_see_all">See all</string> <plurals name="location_app_permission_summary_location_on">
<item quantity="one">
<xliff:g id="background_location_app_count">%1$d</xliff:g>
of
<xliff:g id="total_location_app_count">%2$d</xliff:g>
app has unlimited access</item>
<item quantity="other">
<xliff:g id="background_location_app_count">%1$d</xliff:g>
of
<xliff:g id="total_location_app_count">%2$d</xliff:g>
apps have unlimited access</item>
</plurals>
<!-- [CHAR LIMIT=50] Location settings screen, sub category for recent location access -->
<string name="location_category_recent_location_access">Recent location access</string>
<!-- [CHAR LIMIT=30] Location settings screen, button to bring the user to view the details of recent location access -->
<string name="location_recent_location_access_view_details">View details</string>
<!-- Location settings screen, displayed when there's no recent app accessing location --> <!-- Location settings screen, displayed when there's no recent app accessing location -->
<string name="location_no_recent_apps">No apps have requested location recently</string> <string name="location_no_recent_apps">No apps have requested location recently</string>
<!-- [CHAR LIMIT=30] Location settings screen, sub category for location services -->
<string name="location_category_location_services">Location services</string>
<!-- [CHAR LIMIT=30] Location settings screen, recent location requests high battery use--> <!-- [CHAR LIMIT=30] Location settings screen, recent location requests high battery use-->
<string name="location_high_battery_use">High battery use</string> <string name="location_high_battery_use">High battery use</string>
<!-- [CHAR LIMIT=30] Location settings screen, recent location requests low battery use--> <!-- [CHAR LIMIT=30] Location settings screen, recent location requests low battery use-->

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/location_category_recent_location_requests"
android:key="recent_location_requests_see_all">
<PreferenceCategory
android:key="all_recent_location_requests"/>
</PreferenceScreen>

View File

@@ -20,21 +20,14 @@
android:title="@string/location_settings_title" android:title="@string/location_settings_title"
settings:keywords="@string/keywords_location"> settings:keywords="@string/keywords_location">
<PreferenceCategory <com.android.settingslib.widget.LayoutPreference
android:key="recent_location_requests" android:key="apps_dashboard"
android:title="@string/location_category_recent_location_requests"/> android:layout="@layout/app_entities_header"
settings:allowDividerBelow="true" />
<Preference
android:key="recent_location_requests_see_all_button"
android:title="@string/location_recent_location_requests_see_all"
android:icon="@drawable/ic_chevron_right_24dp"
android:selectable="true"
android:fragment="com.android.settings.location.RecentLocationRequestSeeAllFragment"
settings:searchable="false"/>
<PreferenceCategory <PreferenceCategory
android:key="location_advanced_settings" android:key="location_advanced_settings"
settings:initialExpandedChildrenCount="1"> settings:initialExpandedChildrenCount="0">
<!-- This preference category gets removed if new_recent_location_ui is disabled --> <!-- This preference category gets removed if new_recent_location_ui is disabled -->
<Preference <Preference
@@ -60,8 +53,7 @@
android:selectable="true" /> android:selectable="true" />
<PreferenceCategory <PreferenceCategory
android:key="location_services" android:key="location_services" />
android:title="@string/location_category_location_services"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory

View File

@@ -93,10 +93,11 @@
<Preference <Preference
android:key="top_level_location" android:key="top_level_location"
android:title="@string/location_settings_title" android:title="@string/location_settings_title"
android:summary="@string/location_settings_summary" android:summary="@string/location_settings_loading_app_permission_stats"
android:icon="@drawable/ic_homepage_location" android:icon="@drawable/ic_homepage_location"
android:order="-50" android:order="-50"
android:fragment="com.android.settings.location.LocationSettings"/> android:fragment="com.android.settings.location.LocationSettings"
settings:controller="com.android.settings.location.TopLevelLocationPreferenceController"/>
<Preference <Preference
android:key="top_level_security" android:key="top_level_security"

View File

@@ -1,18 +1,38 @@
package com.android.settings.location; package com.android.settings.location;
import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import android.content.Context; import android.content.Context;
import android.location.LocationManager;
import android.permission.RuntimePermissionPresenter;
import android.provider.Settings; import android.provider.Settings;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
public class AppLocationPermissionPreferenceController extends public class AppLocationPermissionPreferenceController extends
AbstractPreferenceController implements PreferenceControllerMixin { LocationBasePreferenceController implements PreferenceControllerMixin {
private static final String KEY_APP_LEVEL_PERMISSIONS = "app_level_permissions"; private static final String KEY_APP_LEVEL_PERMISSIONS = "app_level_permissions";
/** Total number of apps that has location permission. */
private int mNumTotal = -1;
/** Total number of apps that has background location permission. */
private int mNumBackground = -1;
private final LocationManager mLocationManager;
private Preference mPreference;
public AppLocationPermissionPreferenceController(Context context) { public AppLocationPermissionPreferenceController(Context context, Lifecycle lifecycle) {
super(context); super(context, lifecycle);
mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
} }
@Override @Override
@@ -25,4 +45,53 @@ public class AppLocationPermissionPreferenceController extends
return Settings.Global.getInt(mContext.getContentResolver(), return Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.LOCATION_SETTINGS_LINK_TO_PERMISSIONS_ENABLED, 1) == 1; Settings.Global.LOCATION_SETTINGS_LINK_TO_PERMISSIONS_ENABLED, 1) == 1;
} }
@Override
public CharSequence getSummary() {
if (mLocationManager.isLocationEnabled()) {
if (mNumTotal == -1 || mNumBackground == -1) {
return mContext.getString(R.string.location_settings_loading_app_permission_stats);
}
return mContext.getResources().getQuantityString(
R.plurals.location_app_permission_summary_location_on, mNumBackground,
mNumBackground, mNumTotal);
} else {
return mContext.getString(R.string.location_app_permission_summary_location_off);
}
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
mPreference = preference;
final AtomicInteger loadingInProgress = new AtomicInteger(2);
refreshSummary(preference);
// Bail out if location has been disabled.
if (!mLocationManager.isLocationEnabled()) {
return;
}
RuntimePermissionPresenter.getInstance(mContext).countPermissionApps(
Arrays.asList(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION), false, false,
(numApps) -> {
mNumTotal = numApps;
if (loadingInProgress.decrementAndGet() == 0) {
refreshSummary(preference);
}
}, null);
RuntimePermissionPresenter.getInstance(mContext).countPermissionApps(
Collections.singletonList(ACCESS_BACKGROUND_LOCATION), true, false,
(numApps) -> {
mNumBackground = numApps;
if (loadingInProgress.decrementAndGet() == 0) {
refreshSummary(preference);
}
}, null);
}
@Override
public void onLocationModeChanged(int mode, boolean restricted) {
// 'null' is checked inside updateState(), so no need to check here.
updateState(mPreference);
}
} }

View File

@@ -35,15 +35,15 @@ import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnResume; import com.android.settingslib.core.lifecycle.events.OnStop;
/** /**
* A class that listens to location settings change and modifies location settings * A class that listens to location settings change and modifies location settings
* settings. * settings.
*/ */
public class LocationEnabler implements LifecycleObserver, OnResume, OnPause { public class LocationEnabler implements LifecycleObserver, OnStart, OnStop {
private static final String TAG = "LocationEnabler"; private static final String TAG = "LocationEnabler";
@VisibleForTesting @VisibleForTesting
@@ -73,7 +73,7 @@ public class LocationEnabler implements LifecycleObserver, OnResume, OnPause {
} }
@Override @Override
public void onResume() { public void onStart() {
if (mReceiver == null) { if (mReceiver == null) {
mReceiver = new BroadcastReceiver() { mReceiver = new BroadcastReceiver() {
@Override @Override
@@ -90,12 +90,8 @@ public class LocationEnabler implements LifecycleObserver, OnResume, OnPause {
} }
@Override @Override
public void onPause() { public void onStop() {
try {
mContext.unregisterReceiver(mReceiver); mContext.unregisterReceiver(mReceiver);
} catch (RuntimeException e) {
// Ignore exceptions caused by race condition
}
} }
void refreshLocationMode() { void refreshLocationMode() {

View File

@@ -35,7 +35,7 @@ import com.android.settings.search.Indexable;
import com.android.settings.widget.SwitchBar; import com.android.settings.widget.SwitchBar;
import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.location.RecentLocationApps; import com.android.settingslib.location.RecentLocationAccesses;
import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.search.SearchIndexable;
import java.util.ArrayList; import java.util.ArrayList;
@@ -52,7 +52,7 @@ import java.util.List;
* <li>In switch bar: location master switch. Used to toggle location on and off. * <li>In switch bar: location master switch. Used to toggle location on and off.
* </li> * </li>
* </ul> * </ul>
* <li>Recent location requests: automatically populated by {@link RecentLocationApps}</li> * <li>Recent location requests: automatically populated by {@link RecentLocationAccesses}</li>
* <li>Location services: multi-app settings provided from outside the Android framework. Each * <li>Location services: multi-app settings provided from outside the Android framework. Each
* is injected by a system-partition app via the {@link SettingInjectorService} API.</li> * is injected by a system-partition app via the {@link SettingInjectorService} API.</li>
* </ul> * </ul>
@@ -122,13 +122,11 @@ public class LocationSettings extends DashboardFragment {
private static List<AbstractPreferenceController> buildPreferenceControllers( private static List<AbstractPreferenceController> buildPreferenceControllers(
Context context, LocationSettings fragment, Lifecycle lifecycle) { Context context, LocationSettings fragment, Lifecycle lifecycle) {
final List<AbstractPreferenceController> controllers = new ArrayList<>(); final List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(new AppLocationPermissionPreferenceController(context)); controllers.add(new AppLocationPermissionPreferenceController(context, lifecycle));
controllers.add(new LocationForWorkPreferenceController(context, lifecycle)); controllers.add(new LocationForWorkPreferenceController(context, lifecycle));
controllers.add( controllers.add(new RecentLocationAccessPreferenceController(context));
new RecentLocationRequestPreferenceController(context, fragment, lifecycle));
controllers.add(new LocationScanningPreferenceController(context)); controllers.add(new LocationScanningPreferenceController(context));
controllers.add( controllers.add(new LocationServicePreferenceController(context, fragment, lifecycle));
new LocationServicePreferenceController(context, fragment, lifecycle));
controllers.add(new LocationFooterPreferenceController(context, lifecycle)); controllers.add(new LocationFooterPreferenceController(context, lifecycle));
return controllers; return controllers;
} }

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.settings.location;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.location.RecentLocationAccesses;
import com.android.settingslib.widget.AppEntitiesHeaderController;
import com.android.settingslib.widget.LayoutPreference;
import java.util.List;
public class RecentLocationAccessPreferenceController extends AbstractPreferenceController
implements PreferenceControllerMixin {
/** Key for the recent location apps dashboard */
private static final String KEY_APPS_DASHBOARD = "apps_dashboard";
private final RecentLocationAccesses mRecentLocationAccesses;
private AppEntitiesHeaderController mController;
private static final int MAXIMUM_APP_COUNT = 3;
public RecentLocationAccessPreferenceController(Context context) {
this(context, new RecentLocationAccesses(context));
}
@VisibleForTesting
RecentLocationAccessPreferenceController(Context context,
RecentLocationAccesses recentAccesses) {
super(context);
mRecentLocationAccesses = recentAccesses;
}
@Override
public String getPreferenceKey() {
return KEY_APPS_DASHBOARD;
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
final LayoutPreference preference = (LayoutPreference) screen.findPreference(
KEY_APPS_DASHBOARD);
final View view = preference.findViewById(R.id.app_entities_header);
mController = AppEntitiesHeaderController.newInstance(mContext, view)
.setHeaderTitleRes(R.string.location_category_recent_location_access)
.setHeaderDetailsRes(R.string.location_recent_location_access_view_details)
.setHeaderDetailsClickListener((View v) -> {
final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE);
intent.putExtra(Intent.EXTRA_PERMISSION_NAME,
Manifest.permission.ACCESS_FINE_LOCATION);
mContext.startActivity(intent);
});
}
@Override
public void updateState(Preference preference) {
updateRecentApps();
}
private void updateRecentApps() {
final List<RecentLocationAccesses.Access> recentLocationAccesses =
mRecentLocationAccesses.getAppListSorted();
if (recentLocationAccesses.size() > 0) {
// Display the top 3 preferences to container in original order.
int i = 0;
for (; i < Math.min(recentLocationAccesses.size(), MAXIMUM_APP_COUNT); i++) {
final RecentLocationAccesses.Access access = recentLocationAccesses.get(i);
mController.setAppEntity(i, access.icon, access.label, access.contentDescription);
}
for (; i < MAXIMUM_APP_COUNT; i++) {
mController.removeAppEntity(i);
}
} else {
// If there's no item to display, add a "No recent apps" item.
}
mController.apply();
}
}

View File

@@ -1,155 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.settings.location;
import android.content.Context;
import android.os.Bundle;
import android.os.UserHandle;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.applications.appinfo.AppInfoDashboardFragment;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.location.RecentLocationApps;
import com.android.settingslib.widget.apppreference.AppPreference;
import java.util.List;
public class RecentLocationRequestPreferenceController extends LocationBasePreferenceController {
/** Key for preference category "Recent location requests" */
private static final String KEY_RECENT_LOCATION_REQUESTS = "recent_location_requests";
@VisibleForTesting
static final String KEY_SEE_ALL_BUTTON = "recent_location_requests_see_all_button";
private final LocationSettings mFragment;
private final RecentLocationApps mRecentLocationApps;
private PreferenceCategory mCategoryRecentLocationRequests;
private Preference mSeeAllButton;
/** Used in this class and {@link RecentLocationRequestSeeAllPreferenceController} */
static class PackageEntryClickedListener implements Preference.OnPreferenceClickListener {
private final DashboardFragment mFragment;
private final String mPackage;
private final UserHandle mUserHandle;
public PackageEntryClickedListener(DashboardFragment fragment, String packageName,
UserHandle userHandle) {
mFragment = fragment;
mPackage = packageName;
mUserHandle = userHandle;
}
@Override
public boolean onPreferenceClick(Preference preference) {
// start new fragment to display extended information
final Bundle args = new Bundle();
args.putString(AppInfoDashboardFragment.ARG_PACKAGE_NAME, mPackage);
new SubSettingLauncher(mFragment.getContext())
.setDestination(AppInfoDashboardFragment.class.getName())
.setArguments(args)
.setTitleRes(R.string.application_info_label)
.setUserHandle(mUserHandle)
.setSourceMetricsCategory(mFragment.getMetricsCategory())
.launch();
return true;
}
}
public RecentLocationRequestPreferenceController(Context context, LocationSettings fragment,
Lifecycle lifecycle) {
this(context, fragment, lifecycle, new RecentLocationApps(context));
}
@VisibleForTesting
RecentLocationRequestPreferenceController(Context context, LocationSettings fragment,
Lifecycle lifecycle, RecentLocationApps recentApps) {
super(context, lifecycle);
mFragment = fragment;
mRecentLocationApps = recentApps;
}
@Override
public String getPreferenceKey() {
return KEY_RECENT_LOCATION_REQUESTS;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mCategoryRecentLocationRequests =
(PreferenceCategory) screen.findPreference(KEY_RECENT_LOCATION_REQUESTS);
mSeeAllButton = screen.findPreference(KEY_SEE_ALL_BUTTON);
}
@Override
public void updateState(Preference preference) {
mCategoryRecentLocationRequests.removeAll();
mSeeAllButton.setVisible(false);
final Context prefContext = preference.getContext();
final List<RecentLocationApps.Request> recentLocationRequests =
mRecentLocationApps.getAppListSorted();
if (recentLocationRequests.size() > 3) {
// Display the top 3 preferences to container in original order.
for (int i = 0; i < 3; i++) {
mCategoryRecentLocationRequests.addPreference(
createAppPreference(prefContext, recentLocationRequests.get(i)));
}
// Display a button to list all requests
mSeeAllButton.setVisible(true);
} else if (recentLocationRequests.size() > 0) {
// Add preferences to container in original order (already sorted by recency).
for (RecentLocationApps.Request request : recentLocationRequests) {
mCategoryRecentLocationRequests.addPreference(
createAppPreference(prefContext, request));
}
} else {
// If there's no item to display, add a "No recent apps" item.
final Preference banner = createAppPreference(prefContext);
banner.setTitle(R.string.location_no_recent_apps);
banner.setSelectable(false);
mCategoryRecentLocationRequests.addPreference(banner);
}
}
@Override
public void onLocationModeChanged(int mode, boolean restricted) {
mCategoryRecentLocationRequests.setEnabled(mLocationEnabler.isEnabled(mode));
}
@VisibleForTesting
AppPreference createAppPreference(Context prefContext) {
return new AppPreference(prefContext);
}
@VisibleForTesting
AppPreference createAppPreference(Context prefContext, RecentLocationApps.Request request) {
final AppPreference pref = createAppPreference(prefContext);
pref.setSummary(request.contentDescription);
pref.setIcon(request.icon);
pref.setTitle(request.label);
pref.setOnPreferenceClickListener(new PackageEntryClickedListener(
mFragment, request.packageName, request.userHandle));
return pref;
}
}

View File

@@ -1,93 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.location;
import android.content.Context;
import android.provider.SearchIndexableResource;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.search.Indexable;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.search.SearchIndexable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** Dashboard Fragment to display all recent location requests, sorted by recency. */
@SearchIndexable
public class RecentLocationRequestSeeAllFragment extends DashboardFragment {
private static final String TAG = "RecentLocationReqAll";
public static final String PATH =
"com.android.settings.location.RecentLocationRequestSeeAllFragment";
@Override
public int getMetricsCategory() {
return MetricsEvent.RECENT_LOCATION_REQUESTS_ALL;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.location_recent_requests_see_all;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
return buildPreferenceControllers(context, getSettingsLifecycle(), this);
}
private static List<AbstractPreferenceController> buildPreferenceControllers(
Context context, Lifecycle lifecycle, RecentLocationRequestSeeAllFragment fragment) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(
new RecentLocationRequestSeeAllPreferenceController(context, lifecycle, fragment));
return controllers;
}
/**
* For Search.
*/
public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider() {
@Override
public List<SearchIndexableResource> getXmlResourcesToIndex(
Context context, boolean enabled) {
final SearchIndexableResource sir = new SearchIndexableResource(context);
sir.xmlResId = R.xml.location_recent_requests_see_all;
return Arrays.asList(sir);
}
@Override
public List<AbstractPreferenceController> getPreferenceControllers(Context
context) {
return buildPreferenceControllers(
context, /* lifecycle = */ null, /* fragment = */ null);
}
};
}

View File

@@ -1,98 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.location;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.location.RecentLocationApps;
import com.android.settingslib.widget.apppreference.AppPreference;
import java.util.List;
/** Preference controller for preference category displaying all recent location requests. */
public class RecentLocationRequestSeeAllPreferenceController
extends LocationBasePreferenceController {
/** Key for preference category "All recent location requests" */
private static final String KEY_ALL_RECENT_LOCATION_REQUESTS = "all_recent_location_requests";
private final RecentLocationRequestSeeAllFragment mFragment;
private PreferenceCategory mCategoryAllRecentLocationRequests;
private RecentLocationApps mRecentLocationApps;
public RecentLocationRequestSeeAllPreferenceController(
Context context, Lifecycle lifecycle, RecentLocationRequestSeeAllFragment fragment) {
this(context, lifecycle, fragment, new RecentLocationApps(context));
}
@VisibleForTesting
RecentLocationRequestSeeAllPreferenceController(
Context context,
Lifecycle lifecycle,
RecentLocationRequestSeeAllFragment fragment,
RecentLocationApps recentLocationApps) {
super(context, lifecycle);
mFragment = fragment;
mRecentLocationApps = recentLocationApps;
}
@Override
public String getPreferenceKey() {
return KEY_ALL_RECENT_LOCATION_REQUESTS;
}
@Override
public void onLocationModeChanged(int mode, boolean restricted) {
mCategoryAllRecentLocationRequests.setEnabled(mLocationEnabler.isEnabled(mode));
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mCategoryAllRecentLocationRequests =
(PreferenceCategory) screen.findPreference(KEY_ALL_RECENT_LOCATION_REQUESTS);
}
@Override
public void updateState(Preference preference) {
mCategoryAllRecentLocationRequests.removeAll();
List<RecentLocationApps.Request> requests = mRecentLocationApps.getAppListSorted();
for (RecentLocationApps.Request request : requests) {
Preference appPreference = createAppPreference(preference.getContext(), request);
mCategoryAllRecentLocationRequests.addPreference(appPreference);
}
}
@VisibleForTesting
AppPreference createAppPreference(
Context prefContext, RecentLocationApps.Request request) {
final AppPreference pref = new AppPreference(prefContext);
pref.setSummary(request.contentDescription);
pref.setIcon(request.icon);
pref.setTitle(request.label);
pref.setOnPreferenceClickListener(
new RecentLocationRequestPreferenceController.PackageEntryClickedListener(
mFragment, request.packageName, request.userHandle));
return pref;
}
}

View File

@@ -0,0 +1,99 @@
package com.android.settings.location;
import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.LocationManager;
import android.permission.RuntimePermissionPresenter;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import java.util.Arrays;
import java.util.Collections;
public class TopLevelLocationPreferenceController extends BasePreferenceController implements
LifecycleObserver, OnStart, OnStop {
private static final IntentFilter INTENT_FILTER_LOCATION_MODE_CHANGED =
new IntentFilter(LocationManager.MODE_CHANGED_ACTION);
private final LocationManager mLocationManager;
/** Total number of apps that has location permission. */
private int mNumTotal = -1;
private BroadcastReceiver mReceiver;
private Preference mPreference;
public TopLevelLocationPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public CharSequence getSummary() {
if (mLocationManager.isLocationEnabled()) {
if (mNumTotal == -1) {
return mContext.getString(R.string.location_settings_loading_app_permission_stats);
}
return mContext.getResources().getQuantityString(
R.plurals.location_settings_summary_location_on,
mNumTotal, mNumTotal);
} else {
return mContext.getString(R.string.location_settings_summary_location_off);
}
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
mPreference = preference;
refreshSummary(preference);
// Bail out if location has been disabled.
if (!mLocationManager.isLocationEnabled()) {
return;
}
RuntimePermissionPresenter.getInstance(mContext).countPermissionApps(
Arrays.asList(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION), false, false,
(numApps) -> {
mNumTotal = numApps;
refreshSummary(preference);
}, null);
}
@Override
public void onStart() {
if (mReceiver == null) {
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
refreshLocationMode();
}
};
}
mContext.registerReceiver(mReceiver, INTENT_FILTER_LOCATION_MODE_CHANGED);
refreshLocationMode();
}
@Override
public void onStop() {
mContext.unregisterReceiver(mReceiver);
}
private void refreshLocationMode() {
// 'null' is checked inside updateState(), so no need to check here.
updateState(mPreference);
}
}

View File

@@ -5,6 +5,10 @@ import static com.google.common.truth.Truth.assertThat;
import android.content.Context; import android.content.Context;
import android.provider.Settings; import android.provider.Settings;
import androidx.lifecycle.LifecycleOwner;
import com.android.settingslib.core.lifecycle.Lifecycle;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -21,11 +25,16 @@ public class AppLocationPermissionPreferenceControllerTest {
@Mock @Mock
private Context mContext; private Context mContext;
private LifecycleOwner mLifecycleOwner;
private Lifecycle mLifecycle;
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application; mContext = RuntimeEnvironment.application;
mController = new AppLocationPermissionPreferenceController(mContext); mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner);
mController = new AppLocationPermissionPreferenceController(mContext, mLifecycle);
} }
@Test @Test

View File

@@ -84,30 +84,31 @@ public class LocationEnablerTest {
} }
@Test @Test
public void onResume_shouldSetActiveAndRegisterListener() { public void onStart_shouldSetActiveAndRegisterListener() {
mEnabler.onResume(); mEnabler.onStart();
verify(mContext).registerReceiver(eq(mEnabler.mReceiver), verify(mContext).registerReceiver(eq(mEnabler.mReceiver),
eq(LocationEnabler.INTENT_FILTER_LOCATION_MODE_CHANGED)); eq(LocationEnabler.INTENT_FILTER_LOCATION_MODE_CHANGED));
} }
@Test @Test
public void onResume_shouldRefreshLocationMode() { public void onStart_shouldRefreshLocationMode() {
mEnabler.onResume(); mEnabler.onStart();
verify(mEnabler).refreshLocationMode(); verify(mEnabler).refreshLocationMode();
} }
@Test @Test
public void onPause_shouldUnregisterListener() { public void onStop_shouldUnregisterListener() {
mEnabler.onPause(); mEnabler.onStart();
mEnabler.onStop();
verify(mContext).unregisterReceiver(mEnabler.mReceiver); verify(mContext).unregisterReceiver(mEnabler.mReceiver);
} }
@Test @Test
public void onReceive_shouldRefreshLocationMode() { public void onReceive_shouldRefreshLocationMode() {
mEnabler.onResume(); mEnabler.onStart();
reset(mListener); reset(mListener);
mEnabler.mReceiver.onReceive(mContext, new Intent()); mEnabler.mReceiver.onReceive(mContext, new Intent());

View File

@@ -1,217 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.location;
import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.Intent;
import android.provider.Settings;
import android.text.TextUtils;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.applications.appinfo.AppInfoDashboardFragment;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.location.RecentLocationApps;
import com.android.settingslib.location.RecentLocationApps.Request;
import com.android.settingslib.widget.apppreference.AppPreference;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class RecentLocationRequestPreferenceControllerTest {
@Mock
private LocationSettings mFragment;
@Mock
private PreferenceCategory mCategory;
@Mock
private PreferenceScreen mScreen;
@Mock
private RecentLocationApps mRecentLocationApps;
@Mock
private Preference mSeeAllButton;
private Context mContext;
private RecentLocationRequestPreferenceController mController;
private LifecycleOwner mLifecycleOwner;
private Lifecycle mLifecycle;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner);
mController = spy(new RecentLocationRequestPreferenceController(
mContext, mFragment, mLifecycle, mRecentLocationApps));
when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mCategory);
when(mScreen.findPreference(mController.KEY_SEE_ALL_BUTTON)).thenReturn(mSeeAllButton);
final String key = mController.getPreferenceKey();
when(mCategory.getKey()).thenReturn(key);
when(mCategory.getContext()).thenReturn(mContext);
}
@Test
public void onLocationModeChanged_LocationOn_shouldEnablePreference() {
mController.displayPreference(mScreen);
mController.onLocationModeChanged(Settings.Secure.LOCATION_MODE_BATTERY_SAVING, false);
verify(mCategory).setEnabled(true);
}
@Test
public void onLocationModeChanged_LocationOff_shouldDisablePreference() {
mController.displayPreference(mScreen);
mController.onLocationModeChanged(Settings.Secure.LOCATION_MODE_OFF, false);
verify(mCategory).setEnabled(false);
}
@Test
public void updateState_noRecentRequest_shouldRemoveAllAndAddBanner() {
doReturn(new ArrayList<>()).when(mRecentLocationApps).getAppListSorted();
mController.displayPreference(mScreen);
mController.updateState(mCategory);
verify(mCategory).removeAll();
final String title = mContext.getString(R.string.location_no_recent_apps);
verify(mCategory).addPreference(argThat(titleMatches(title)));
}
@Test
public void updateState_hasRecentRequest_shouldRemoveAllAndAddInjectedSettings() {
List<Request> requests = createMockRequests(2);
doReturn(requests).when(mRecentLocationApps).getAppListSorted();
mController.displayPreference(mScreen);
mController.updateState(mCategory);
verify(mCategory).removeAll();
// Verifies two preferences are added in original order
InOrder inOrder = Mockito.inOrder(mCategory);
inOrder.verify(mCategory).addPreference(argThat(titleMatches("appTitle0")));
inOrder.verify(mCategory).addPreference(argThat(titleMatches("appTitle1")));
}
@Test
public void updateState_hasOverThreeRequests_shouldDisplaySeeAllButton() {
List<Request> requests = createMockRequests(6);
when(mRecentLocationApps.getAppListSorted()).thenReturn(requests);
mController.displayPreference(mScreen);
mController.updateState(mCategory);
verify(mCategory).removeAll();
// Verifies the first three preferences are added
InOrder inOrder = Mockito.inOrder(mCategory);
inOrder.verify(mCategory).addPreference(argThat(titleMatches("appTitle0")));
inOrder.verify(mCategory).addPreference(argThat(titleMatches("appTitle1")));
inOrder.verify(mCategory).addPreference(argThat(titleMatches("appTitle2")));
verify(mCategory, never()).addPreference(argThat(titleMatches("appTitle3")));
// Verifies the "See all" preference is visible
verify(mSeeAllButton).setVisible(true);
}
@Test
public void createAppPreference_shouldAddClickListener() {
final Request request = mock(Request.class);
final AppPreference preference = mock(AppPreference.class);
doReturn(preference).when(mController).createAppPreference(any(Context.class));
mController.createAppPreference(mContext, request);
verify(preference).setOnPreferenceClickListener(
any(RecentLocationRequestPreferenceController.PackageEntryClickedListener.class));
}
@Test
public void onPreferenceClick_shouldLaunchAppDetails() {
final Context context = mock(Context.class);
when(mFragment.getContext()).thenReturn(context);
final List<RecentLocationApps.Request> requests = new ArrayList<>();
final Request request = mock(Request.class);
requests.add(request);
doReturn(requests).when(mRecentLocationApps).getAppListSorted();
final AppPreference preference = new AppPreference(mContext);
doReturn(preference).when(mController).createAppPreference(any(Context.class));
mController.displayPreference(mScreen);
mController.updateState(mCategory);
final ArgumentCaptor<Intent> intent = ArgumentCaptor.forClass(Intent.class);
preference.performClick();
verify(context).startActivity(intent.capture());
assertThat(intent.getValue().getStringExtra(EXTRA_SHOW_FRAGMENT))
.isEqualTo(AppInfoDashboardFragment.class.getName());
}
private static ArgumentMatcher<Preference> titleMatches(String expected) {
return preference -> TextUtils.equals(expected, preference.getTitle());
}
private List<RecentLocationApps.Request> createMockRequests(int count) {
List<RecentLocationApps.Request> requests = new ArrayList<>();
for (int i = 0; i < count; i++) {
// Add mock requests
Request req = mock(Request.class, "request" + i);
requests.add(req);
// Map mock AppPreferences with mock requests
String title = "appTitle" + i;
AppPreference appPreference = mock(AppPreference.class, "AppPreference" + i);
doReturn(title).when(appPreference).getTitle();
doReturn(appPreference)
.when(mController).createAppPreference(any(Context.class), eq(req));
}
return requests;
}
}

View File

@@ -1,126 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.location;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.provider.Settings.Secure;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.location.RecentLocationApps;
import com.android.settingslib.location.RecentLocationApps.Request;
import com.android.settingslib.widget.apppreference.AppPreference;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.Collections;
/** Unit tests for {@link RecentLocationRequestSeeAllPreferenceController} */
@RunWith(RobolectricTestRunner.class)
public class RecentLocationRequestSeeAllPreferenceControllerTest {
@Mock
RecentLocationRequestSeeAllFragment mFragment;
@Mock
private PreferenceScreen mScreen;
@Mock
private PreferenceCategory mCategory;
@Mock
private RecentLocationApps mRecentLocationApps;
private Context mContext;
private LifecycleOwner mLifecycleOwner;
private Lifecycle mLifecycle;
private RecentLocationRequestSeeAllPreferenceController mController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner);
mController = spy(
new RecentLocationRequestSeeAllPreferenceController(
mContext, mLifecycle, mFragment, mRecentLocationApps));
when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mCategory);
final String key = mController.getPreferenceKey();
when(mCategory.getKey()).thenReturn(key);
when(mCategory.getContext()).thenReturn(mContext);
}
@Test
public void onLocationModeChanged_locationOn_shouldEnablePreference() {
mController.displayPreference(mScreen);
mController.onLocationModeChanged(Secure.LOCATION_MODE_HIGH_ACCURACY, false);
verify(mCategory).setEnabled(true);
}
@Test
public void onLocationModeChanged_locationOff_shouldDisablePreference() {
mController.displayPreference(mScreen);
mController.onLocationModeChanged(Secure.LOCATION_MODE_OFF, false);
verify(mCategory).setEnabled(false);
}
@Test
public void updateState_shouldRemoveAll() {
doReturn(Collections.EMPTY_LIST).when(mRecentLocationApps).getAppListSorted();
mController.displayPreference(mScreen);
mController.updateState(mCategory);
verify(mCategory).removeAll();
}
@Test
public void updateState_hasRecentLocationRequest_shouldAddPreference() {
Request request = mock(Request.class);
AppPreference appPreference = mock(AppPreference.class);
doReturn(appPreference)
.when(mController).createAppPreference(any(Context.class), eq(request));
when(mRecentLocationApps.getAppListSorted())
.thenReturn(new ArrayList<>(Collections.singletonList(request)));
mController.displayPreference(mScreen);
mController.updateState(mCategory);
verify(mCategory).removeAll();
verify(mCategory).addPreference(appPreference);
}
}