Sort "Recent Location Requests" by recency.

The list of apps with recent location requests is currently displayed in
alphabetical order on device (Settings -> Security & Location ->
Location). Sort this list by recency: (1) apps that are currently
requesting location are displayed  on the top. (2) For apps that are not currently
requesting location, the ones with most recent location request finish
time come first.

Bug: 70350519
Test: Robolectric
Test: manual
Change-Id: I5c757defcd7645d254c9c47e3c83f7e323247a71
This commit is contained in:
Maggie
2017-12-11 18:02:36 -08:00
parent d538bbcbdd
commit 704c423ca3
2 changed files with 193 additions and 16 deletions

View File

@@ -16,21 +16,21 @@
package com.android.settingslib.location;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.support.annotation.VisibleForTesting;
import android.util.IconDrawableFactory;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
@@ -38,11 +38,13 @@ import java.util.List;
*/
public class RecentLocationApps {
private static final String TAG = RecentLocationApps.class.getSimpleName();
private static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
@VisibleForTesting
static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
private static final int RECENT_TIME_INTERVAL_MILLIS = 15 * 60 * 1000;
private static final int[] LOCATION_OPS = new int[] {
@VisibleForTesting
static final int[] LOCATION_OPS = new int[] {
AppOpsManager.OP_MONITOR_LOCATION,
AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION,
};
@@ -59,6 +61,7 @@ public class RecentLocationApps {
/**
* Fills a list of applications which queried location recently within specified time.
* Apps are sorted by recency. Apps with more recent location requests are in the front.
*/
public List<Request> getAppList() {
// Retrieve a location usage list from AppOps
@@ -91,7 +94,18 @@ public class RecentLocationApps {
requests.add(request);
}
}
return requests;
}
public List<Request> getAppListSorted() {
List<Request> requests = getAppList();
// Sort the list of Requests by recency. Most recent request first.
Collections.sort(requests, Collections.reverseOrder(new Comparator<Request>() {
@Override
public int compare(Request request1, Request request2) {
return Long.compare(request1.requestFinishTime, request2.requestFinishTime);
}
}));
return requests;
}
@@ -108,10 +122,12 @@ public class RecentLocationApps {
List<AppOpsManager.OpEntry> entries = ops.getOps();
boolean highBattery = false;
boolean normalBattery = false;
long locationRequestFinishTime = 0L;
// Earliest time for a location request to end and still be shown in list.
long recentLocationCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS;
for (AppOpsManager.OpEntry entry : entries) {
if (entry.isRunning() || entry.getTime() >= recentLocationCutoffTime) {
locationRequestFinishTime = entry.getTime() + entry.getDuration();
switch (entry.getOp()) {
case AppOpsManager.OP_MONITOR_LOCATION:
normalBattery = true;
@@ -133,15 +149,13 @@ public class RecentLocationApps {
}
// The package is fresh enough, continue.
int uid = ops.getUid();
int userId = UserHandle.getUserId(uid);
Request request = null;
try {
IPackageManager ipm = AppGlobals.getPackageManager();
ApplicationInfo appInfo =
ipm.getApplicationInfo(packageName, PackageManager.GET_META_DATA, userId);
ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
packageName, PackageManager.GET_META_DATA, userId);
if (appInfo == null) {
Log.w(TAG, "Null application info retrieved for package " + packageName
+ ", userId " + userId);
@@ -158,12 +172,10 @@ public class RecentLocationApps {
badgedAppLabel = null;
}
request = new Request(packageName, userHandle, icon, appLabel, highBattery,
badgedAppLabel);
} catch (RemoteException e) {
Log.w(TAG, "Error while retrieving application info for package " + packageName
+ ", userId " + userId, e);
badgedAppLabel, locationRequestFinishTime);
} catch (NameNotFoundException e) {
Log.w(TAG, "package name not found for " + packageName + ", userId " + userId);
}
return request;
}
@@ -174,15 +186,18 @@ public class RecentLocationApps {
public final CharSequence label;
public final boolean isHighBattery;
public final CharSequence contentDescription;
public final long requestFinishTime;
private Request(String packageName, UserHandle userHandle, Drawable icon,
CharSequence label, boolean isHighBattery, CharSequence contentDescription) {
CharSequence label, boolean isHighBattery, CharSequence contentDescription,
long requestFinishTime) {
this.packageName = packageName;
this.userHandle = userHandle;
this.icon = icon;
this.label = label;
this.isHighBattery = isHighBattery;
this.contentDescription = contentDescription;
this.requestFinishTime = requestFinishTime;
}
}
}

View File

@@ -0,0 +1,162 @@
package com.android.settingslib.location;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.when;
import static com.google.common.truth.Truth.assertThat;
import android.app.AppOpsManager;
import android.app.AppOpsManager.OpEntry;
import android.app.AppOpsManager.PackageOps;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import com.android.settingslib.SettingsLibRobolectricTestRunner;
import com.android.settingslib.TestConfig;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
@RunWith(SettingsLibRobolectricTestRunner.class)
@Config(
manifest = TestConfig.MANIFEST_PATH,
sdk = TestConfig.SDK_VERSION)
public class RecentLocationAppsTest {
private static final int TEST_UID = 1234;
private static final long NOW = System.currentTimeMillis();
// App running duration in milliseconds
private static final int DURATION = 10;
private static final long ONE_MIN_AGO = NOW - TimeUnit.MINUTES.toMillis(1);
private static final long FOURTEEN_MIN_AGO = NOW - TimeUnit.MINUTES.toMillis(14);
private static final long TWENTY_MIN_AGO = NOW - TimeUnit.MINUTES.toMillis(20);
private static final String[] TEST_PACKAGE_NAMES =
{"package_1MinAgo", "package_14MinAgo", "package_20MinAgo"};
@Mock
private Context mContext;
@Mock
private PackageManager mPackageManager;
@Mock
private AppOpsManager mAppOpsManager;
@Mock
private Resources mResources;
@Mock
private UserManager mUserManager;
private int mTestUserId;
private RecentLocationApps mRecentLocationApps;
@Before
public void setUp() throws NameNotFoundException {
MockitoAnnotations.initMocks(this);
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManager);
when(mContext.getResources()).thenReturn(mResources);
when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
when(mPackageManager.getApplicationLabel(isA(ApplicationInfo.class)))
.thenReturn("testApplicationLabel");
when(mPackageManager.getUserBadgedLabel(isA(CharSequence.class), isA(UserHandle.class)))
.thenReturn("testUserBadgedLabel");
mTestUserId = UserHandle.getUserId(TEST_UID);
when(mUserManager.getUserProfiles())
.thenReturn(Collections.singletonList(new UserHandle(mTestUserId)));
long[] testRequestTime = {ONE_MIN_AGO, FOURTEEN_MIN_AGO, TWENTY_MIN_AGO};
List<PackageOps> appOps = createTestPackageOpsList(TEST_PACKAGE_NAMES, testRequestTime);
when(mAppOpsManager.getPackagesForOps(RecentLocationApps.LOCATION_OPS)).thenReturn(appOps);
mockTestApplicationInfos(mTestUserId, TEST_PACKAGE_NAMES);
mRecentLocationApps = new RecentLocationApps(mContext);
}
@Test
public void testGetAppList_shouldFilterRecentApps() {
List<RecentLocationApps.Request> requests = mRecentLocationApps.getAppList();
// Only two of the apps have requested location within 15 min.
assertThat(requests).hasSize(2);
// Make sure apps are ordered by recency
assertThat(requests.get(0).packageName).isEqualTo(TEST_PACKAGE_NAMES[0]);
assertThat(requests.get(0).requestFinishTime).isEqualTo(ONE_MIN_AGO + DURATION);
assertThat(requests.get(1).packageName).isEqualTo(TEST_PACKAGE_NAMES[1]);
assertThat(requests.get(1).requestFinishTime).isEqualTo(FOURTEEN_MIN_AGO + DURATION);
}
@Test
public void testGetAppList_shouldNotShowAndroidOS() throws NameNotFoundException {
// Add android OS to the list of apps.
PackageOps androidSystemPackageOps =
createPackageOps(
RecentLocationApps.ANDROID_SYSTEM_PACKAGE_NAME,
Process.SYSTEM_UID,
AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION,
ONE_MIN_AGO,
DURATION);
long[] testRequestTime =
{ONE_MIN_AGO, FOURTEEN_MIN_AGO, TWENTY_MIN_AGO, ONE_MIN_AGO};
List<PackageOps> appOps = createTestPackageOpsList(TEST_PACKAGE_NAMES, testRequestTime);
appOps.add(androidSystemPackageOps);
when(mAppOpsManager.getPackagesForOps(RecentLocationApps.LOCATION_OPS)).thenReturn(appOps);
mockTestApplicationInfos(
Process.SYSTEM_UID, RecentLocationApps.ANDROID_SYSTEM_PACKAGE_NAME);
List<RecentLocationApps.Request> requests = mRecentLocationApps.getAppList();
// Android OS shouldn't show up in the list of apps.
assertThat(requests).hasSize(2);
// Make sure apps are ordered by recency
assertThat(requests.get(0).packageName).isEqualTo(TEST_PACKAGE_NAMES[0]);
assertThat(requests.get(0).requestFinishTime).isEqualTo(ONE_MIN_AGO + DURATION);
assertThat(requests.get(1).packageName).isEqualTo(TEST_PACKAGE_NAMES[1]);
assertThat(requests.get(1).requestFinishTime).isEqualTo(FOURTEEN_MIN_AGO + DURATION);
}
private void mockTestApplicationInfos(int userId, String... packageNameList)
throws NameNotFoundException {
for (String packageName : packageNameList) {
ApplicationInfo appInfo = new ApplicationInfo();
appInfo.packageName = packageName;
when(mPackageManager.getApplicationInfoAsUser(
packageName, PackageManager.GET_META_DATA, userId)).thenReturn(appInfo);
}
}
private List<PackageOps> createTestPackageOpsList(String[] packageNameList, long[] time) {
List<PackageOps> packageOpsList = new ArrayList<>();
for (int i = 0; i < packageNameList.length ; i++) {
PackageOps packageOps = createPackageOps(
packageNameList[i],
TEST_UID,
AppOpsManager.OP_MONITOR_LOCATION,
time[i],
DURATION);
packageOpsList.add(packageOps);
}
return packageOpsList;
}
private PackageOps createPackageOps(
String packageName, int uid, int op, long time, int duration) {
return new PackageOps(
packageName,
uid,
Collections.singletonList(createOpEntryWithTime(op, time, duration)));
}
private OpEntry createOpEntryWithTime(int op, long time, int duration) {
return new OpEntry(op, AppOpsManager.MODE_ALLOWED, time, 0L, duration, 0, "");
}
}