From 811d95c373f94c0aef9795634fa9de2283465165 Mon Sep 17 00:00:00 2001 From: Lifu Tang Date: Mon, 10 Dec 2018 16:38:33 -0800 Subject: [PATCH 1/2] Display recent location access in the widget Bug: 120239674 Test: manually Change-Id: Iaf899486bf27c55189eea4c0e913ff1baaf529e5 --- res/values/strings.xml | 10 +- res/xml/location_recent_requests_see_all.xml | 24 -- res/xml/location_settings.xml | 20 +- .../settings/location/LocationSettings.java | 10 +- ...entLocationAccessPreferenceController.java | 103 +++++++++ ...ntLocationRequestPreferenceController.java | 155 ------------- .../RecentLocationRequestSeeAllFragment.java | 93 -------- ...tionRequestSeeAllPreferenceController.java | 98 -------- ...cationRequestPreferenceControllerTest.java | 217 ------------------ ...RequestSeeAllPreferenceControllerTest.java | 126 ---------- 10 files changed, 117 insertions(+), 739 deletions(-) delete mode 100644 res/xml/location_recent_requests_see_all.xml create mode 100644 src/com/android/settings/location/RecentLocationAccessPreferenceController.java delete mode 100644 src/com/android/settings/location/RecentLocationRequestPreferenceController.java delete mode 100644 src/com/android/settings/location/RecentLocationRequestSeeAllFragment.java delete mode 100644 src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceController.java delete mode 100644 tests/robotests/src/com/android/settings/location/RecentLocationRequestPreferenceControllerTest.java delete mode 100644 tests/robotests/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 57897dc0ffe..4abea447d9f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3631,14 +3631,12 @@ App-level permissions - - Recent location requests - - See all + + Recent location access + + View details No apps have requested location recently - - Location services High battery use diff --git a/res/xml/location_recent_requests_see_all.xml b/res/xml/location_recent_requests_see_all.xml deleted file mode 100644 index 38db1426995..00000000000 --- a/res/xml/location_recent_requests_see_all.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/res/xml/location_settings.xml b/res/xml/location_settings.xml index b53e9861642..f1c13cf6ecd 100644 --- a/res/xml/location_settings.xml +++ b/res/xml/location_settings.xml @@ -20,21 +20,14 @@ android:title="@string/location_settings_title" settings:keywords="@string/keywords_location"> - - - + + settings:initialExpandedChildrenCount="0"> + android:key="location_services" /> In switch bar: location master switch. Used to toggle location on and off. * * - *
  • Recent location requests: automatically populated by {@link RecentLocationApps}
  • + *
  • Recent location requests: automatically populated by {@link RecentLocationAccesses}
  • *
  • Location services: multi-app settings provided from outside the Android framework. Each * is injected by a system-partition app via the {@link SettingInjectorService} API.
  • * @@ -124,11 +124,9 @@ public class LocationSettings extends DashboardFragment { final List controllers = new ArrayList<>(); controllers.add(new AppLocationPermissionPreferenceController(context)); controllers.add(new LocationForWorkPreferenceController(context, lifecycle)); - controllers.add( - new RecentLocationRequestPreferenceController(context, fragment, lifecycle)); + controllers.add(new RecentLocationAccessPreferenceController(context)); controllers.add(new LocationScanningPreferenceController(context)); - controllers.add( - new LocationServicePreferenceController(context, fragment, lifecycle)); + controllers.add(new LocationServicePreferenceController(context, fragment, lifecycle)); controllers.add(new LocationFooterPreferenceController(context, lifecycle)); return controllers; } diff --git a/src/com/android/settings/location/RecentLocationAccessPreferenceController.java b/src/com/android/settings/location/RecentLocationAccessPreferenceController.java new file mode 100644 index 00000000000..0d5cca50f99 --- /dev/null +++ b/src/com/android/settings/location/RecentLocationAccessPreferenceController.java @@ -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 = + 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(); + } +} diff --git a/src/com/android/settings/location/RecentLocationRequestPreferenceController.java b/src/com/android/settings/location/RecentLocationRequestPreferenceController.java deleted file mode 100644 index 60374eb5dda..00000000000 --- a/src/com/android/settings/location/RecentLocationRequestPreferenceController.java +++ /dev/null @@ -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 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; - } -} diff --git a/src/com/android/settings/location/RecentLocationRequestSeeAllFragment.java b/src/com/android/settings/location/RecentLocationRequestSeeAllFragment.java deleted file mode 100644 index d256b9b804e..00000000000 --- a/src/com/android/settings/location/RecentLocationRequestSeeAllFragment.java +++ /dev/null @@ -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 createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getSettingsLifecycle(), this); - } - - private static List buildPreferenceControllers( - Context context, Lifecycle lifecycle, RecentLocationRequestSeeAllFragment fragment) { - final List 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 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 getPreferenceControllers(Context - context) { - return buildPreferenceControllers( - context, /* lifecycle = */ null, /* fragment = */ null); - } - }; -} diff --git a/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceController.java b/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceController.java deleted file mode 100644 index 3fa0f00e610..00000000000 --- a/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceController.java +++ /dev/null @@ -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 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; - } -} diff --git a/tests/robotests/src/com/android/settings/location/RecentLocationRequestPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/location/RecentLocationRequestPreferenceControllerTest.java deleted file mode 100644 index d4b4ac31bd1..00000000000 --- a/tests/robotests/src/com/android/settings/location/RecentLocationRequestPreferenceControllerTest.java +++ /dev/null @@ -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 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 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 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 = 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 titleMatches(String expected) { - return preference -> TextUtils.equals(expected, preference.getTitle()); - } - - private List createMockRequests(int count) { - List 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; - } -} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceControllerTest.java deleted file mode 100644 index 7411afe1b4b..00000000000 --- a/tests/robotests/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceControllerTest.java +++ /dev/null @@ -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); - } -} From 3da8f8d31dc78f39cc598cce6b0c9012da1e64af Mon Sep 17 00:00:00 2001 From: Lifu Tang Date: Tue, 11 Dec 2018 13:50:34 -0800 Subject: [PATCH 2/2] Display app stats for location permission Bug: 120221631 Test: manually Change-Id: I53f43079807759c50eeb62029bb0d8d1f84e1118 --- res/values/strings.xml | 28 +++++- res/xml/top_level_settings.xml | 7 +- ...ocationPermissionPreferenceController.java | 77 ++++++++++++++- .../settings/location/LocationEnabler.java | 16 ++- .../settings/location/LocationSettings.java | 2 +- .../TopLevelLocationPreferenceController.java | 99 +++++++++++++++++++ ...ionPermissionPreferenceControllerTest.java | 11 ++- .../location/LocationEnablerTest.java | 15 +-- 8 files changed, 226 insertions(+), 29 deletions(-) create mode 100644 src/com/android/settings/location/TopLevelLocationPreferenceController.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 4abea447d9f..be9cc14754e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -827,8 +827,15 @@ Location Use location - - Scanning, location history + + Off + + + On - %1$d app can access location + On - %1$d apps can access location + + + Loading\u2026 Accounts @@ -3630,7 +3637,22 @@ Location for work profile - App-level permissions + App permission + + Location is off + + + + %1$d + of + %2$d + app has unlimited access + + %1$d + of + %2$d + apps have unlimited access + Recent location access diff --git a/res/xml/top_level_settings.xml b/res/xml/top_level_settings.xml index 03e32dcfb27..9f4f90279f8 100644 --- a/res/xml/top_level_settings.xml +++ b/res/xml/top_level_settings.xml @@ -93,10 +93,11 @@ + android:fragment="com.android.settings.location.LocationSettings" + settings:controller="com.android.settings.location.TopLevelLocationPreferenceController"/> - \ No newline at end of file + diff --git a/src/com/android/settings/location/AppLocationPermissionPreferenceController.java b/src/com/android/settings/location/AppLocationPermissionPreferenceController.java index f920fdc7dc0..5bfc58447e1 100644 --- a/src/com/android/settings/location/AppLocationPermissionPreferenceController.java +++ b/src/com/android/settings/location/AppLocationPermissionPreferenceController.java @@ -1,18 +1,38 @@ 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.location.LocationManager; +import android.permission.RuntimePermissionPresenter; import android.provider.Settings; +import androidx.preference.Preference; + +import com.android.settings.R; 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 - AbstractPreferenceController implements PreferenceControllerMixin { + LocationBasePreferenceController implements PreferenceControllerMixin { 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) { - super(context); + public AppLocationPermissionPreferenceController(Context context, Lifecycle lifecycle) { + super(context, lifecycle); + mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); } @Override @@ -25,4 +45,53 @@ public class AppLocationPermissionPreferenceController extends return Settings.Global.getInt(mContext.getContentResolver(), 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); + } } diff --git a/src/com/android/settings/location/LocationEnabler.java b/src/com/android/settings/location/LocationEnabler.java index 20c228024ff..e1bdf162ef2 100644 --- a/src/com/android/settings/location/LocationEnabler.java +++ b/src/com/android/settings/location/LocationEnabler.java @@ -35,15 +35,15 @@ import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnPause; -import com.android.settingslib.core.lifecycle.events.OnResume; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; /** * A class that listens to location settings change and modifies location settings * settings. */ -public class LocationEnabler implements LifecycleObserver, OnResume, OnPause { +public class LocationEnabler implements LifecycleObserver, OnStart, OnStop { private static final String TAG = "LocationEnabler"; @VisibleForTesting @@ -73,7 +73,7 @@ public class LocationEnabler implements LifecycleObserver, OnResume, OnPause { } @Override - public void onResume() { + public void onStart() { if (mReceiver == null) { mReceiver = new BroadcastReceiver() { @Override @@ -90,12 +90,8 @@ public class LocationEnabler implements LifecycleObserver, OnResume, OnPause { } @Override - public void onPause() { - try { - mContext.unregisterReceiver(mReceiver); - } catch (RuntimeException e) { - // Ignore exceptions caused by race condition - } + public void onStop() { + mContext.unregisterReceiver(mReceiver); } void refreshLocationMode() { diff --git a/src/com/android/settings/location/LocationSettings.java b/src/com/android/settings/location/LocationSettings.java index 8a92f4706eb..41123402039 100644 --- a/src/com/android/settings/location/LocationSettings.java +++ b/src/com/android/settings/location/LocationSettings.java @@ -122,7 +122,7 @@ public class LocationSettings extends DashboardFragment { private static List buildPreferenceControllers( Context context, LocationSettings fragment, Lifecycle lifecycle) { final List controllers = new ArrayList<>(); - controllers.add(new AppLocationPermissionPreferenceController(context)); + controllers.add(new AppLocationPermissionPreferenceController(context, lifecycle)); controllers.add(new LocationForWorkPreferenceController(context, lifecycle)); controllers.add(new RecentLocationAccessPreferenceController(context)); controllers.add(new LocationScanningPreferenceController(context)); diff --git a/src/com/android/settings/location/TopLevelLocationPreferenceController.java b/src/com/android/settings/location/TopLevelLocationPreferenceController.java new file mode 100644 index 00000000000..d0bd9a92843 --- /dev/null +++ b/src/com/android/settings/location/TopLevelLocationPreferenceController.java @@ -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); + } +} diff --git a/tests/robotests/src/com/android/settings/location/AppLocationPermissionPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/location/AppLocationPermissionPreferenceControllerTest.java index eff5d4337ed..6379e445f4b 100644 --- a/tests/robotests/src/com/android/settings/location/AppLocationPermissionPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/location/AppLocationPermissionPreferenceControllerTest.java @@ -5,6 +5,10 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.provider.Settings; +import androidx.lifecycle.LifecycleOwner; + +import com.android.settingslib.core.lifecycle.Lifecycle; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -21,11 +25,16 @@ public class AppLocationPermissionPreferenceControllerTest { @Mock private Context mContext; + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + @Before public void setUp() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; - mController = new AppLocationPermissionPreferenceController(mContext); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + mController = new AppLocationPermissionPreferenceController(mContext, mLifecycle); } @Test diff --git a/tests/robotests/src/com/android/settings/location/LocationEnablerTest.java b/tests/robotests/src/com/android/settings/location/LocationEnablerTest.java index e3808305982..806e2ecf980 100644 --- a/tests/robotests/src/com/android/settings/location/LocationEnablerTest.java +++ b/tests/robotests/src/com/android/settings/location/LocationEnablerTest.java @@ -84,30 +84,31 @@ public class LocationEnablerTest { } @Test - public void onResume_shouldSetActiveAndRegisterListener() { - mEnabler.onResume(); + public void onStart_shouldSetActiveAndRegisterListener() { + mEnabler.onStart(); verify(mContext).registerReceiver(eq(mEnabler.mReceiver), eq(LocationEnabler.INTENT_FILTER_LOCATION_MODE_CHANGED)); } @Test - public void onResume_shouldRefreshLocationMode() { - mEnabler.onResume(); + public void onStart_shouldRefreshLocationMode() { + mEnabler.onStart(); verify(mEnabler).refreshLocationMode(); } @Test - public void onPause_shouldUnregisterListener() { - mEnabler.onPause(); + public void onStop_shouldUnregisterListener() { + mEnabler.onStart(); + mEnabler.onStop(); verify(mContext).unregisterReceiver(mEnabler.mReceiver); } @Test public void onReceive_shouldRefreshLocationMode() { - mEnabler.onResume(); + mEnabler.onStart(); reset(mListener); mEnabler.mReceiver.onReceive(mContext, new Intent());