App open event utils for fetching all app open events in the last day.
go/appsearch-learning-nicknames -> go/app-open-event-indexer.
Bug: 357835538
Flag: com.android.appsearch.flags.app_open_event_indexer_enabled
Change-Id: I4cc35ee65a2e8f18d54821f5a46e048ce41398ee
diff --git a/framework/java/external/android/app/appsearch/SetSchemaRequest.java b/framework/java/external/android/app/appsearch/SetSchemaRequest.java
index bed3f4e..f8c72a4 100644
--- a/framework/java/external/android/app/appsearch/SetSchemaRequest.java
+++ b/framework/java/external/android/app/appsearch/SetSchemaRequest.java
@@ -25,10 +25,8 @@
import android.app.appsearch.annotation.CanIgnoreReturnValue;
import android.util.ArrayMap;
import android.util.ArraySet;
-
import com.android.appsearch.flags.Flags;
import com.android.internal.util.Preconditions;
-
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
@@ -108,6 +106,7 @@
MANAGED_PROFILE_CONTACTS_ACCESS,
EXECUTE_APP_FUNCTIONS,
EXECUTE_APP_FUNCTIONS_TRUSTED,
+ PACKAGE_USAGE_STATS,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AppSearchSupportedPermission {}
@@ -198,6 +197,14 @@
*/
public static final int EXECUTE_APP_FUNCTIONS_TRUSTED = 10;
+ /**
+ * The {@link android.Manifest.permission#PACKAGE_USAGE_STATS} AppSearch supported in {@link
+ * SetSchemaRequest.Builder#addRequiredPermissionsForSchemaTypeVisibility}
+ *
+ * @hide
+ */
+ public static final int PACKAGE_USAGE_STATS = 11;
+
private final Set<AppSearchSchema> mSchemas;
private final Set<String> mSchemasNotDisplayedBySystem;
private final Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;
@@ -475,7 +482,7 @@
Objects.requireNonNull(permissions);
for (int permission : permissions) {
Preconditions.checkArgumentInRange(
- permission, READ_SMS, EXECUTE_APP_FUNCTIONS_TRUSTED, "permission");
+ permission, READ_SMS, PACKAGE_USAGE_STATS, "permission");
}
resetIfBuilt();
Set<Set<Integer>> visibleToPermissions = mSchemasVisibleToPermissions.get(schemaType);
diff --git a/service/java/com/android/server/appsearch/appsindexer/AppsUtil.java b/service/java/com/android/server/appsearch/appsindexer/AppsUtil.java
index 7017369..13de3ff 100644
--- a/service/java/com/android/server/appsearch/appsindexer/AppsUtil.java
+++ b/service/java/com/android/server/appsearch/appsindexer/AppsUtil.java
@@ -19,6 +19,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.appsearch.util.LogUtil;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Intent;
@@ -33,11 +35,9 @@
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
-
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionStaticMetadata;
import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
-
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
@@ -276,6 +276,41 @@
return appFunctions;
}
+ /**
+ * Gets a map of package name to a list of app open timestamps within a specific time range.
+ *
+ * @param usageStatsManager the {@link UsageStatsManager} to query for app open events.
+ * @param startTime the start time in milliseconds since the epoch.
+ * @param endTime the end time in milliseconds since the epoch.
+ * @return a map of package name to a list of app open timestamps.
+ */
+ @NonNull
+ public static Map<String, List<Long>> getAppOpenTimestamps(
+ @NonNull UsageStatsManager usageStatsManager, long startTime, long endTime) {
+
+ Map<String, List<Long>> appOpenTimestamps = new ArrayMap<>();
+
+ UsageEvents usageEvents = usageStatsManager.queryEvents(startTime, endTime);
+ while (usageEvents.hasNextEvent()) {
+ UsageEvents.Event event = new UsageEvents.Event();
+ usageEvents.getNextEvent(event);
+
+ if (event.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND
+ || event.getEventType() == UsageEvents.Event.ACTIVITY_RESUMED) {
+ String packageName = event.getPackageName();
+
+ List<Long> timestamps = appOpenTimestamps.get(packageName);
+ if (timestamps == null) {
+ timestamps = new ArrayList<>();
+ appOpenTimestamps.put(packageName, timestamps);
+ }
+ timestamps.add(event.getTimeStamp());
+ }
+ }
+
+ return appOpenTimestamps;
+ }
+
/** Gets the SHA-256 certificate from a {@link PackageManager}, or null if it is not found */
@Nullable
public static byte[] getCertificate(@NonNull PackageInfo packageInfo) {
@@ -350,4 +385,3 @@
return builder.build();
}
}
-
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsUtilTest.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsUtilTest.java
index 94487a9..06abe0c 100644
--- a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsUtilTest.java
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/AppsUtilTest.java
@@ -19,14 +19,17 @@
import static com.android.server.appsearch.appsindexer.TestUtils.createFakeAppFunctionResolveInfo;
import static com.android.server.appsearch.appsindexer.TestUtils.createFakeLaunchResolveInfo;
import static com.android.server.appsearch.appsindexer.TestUtils.createFakePackageInfo;
+import static com.android.server.appsearch.appsindexer.TestUtils.createIndividualUsageEvent;
+import static com.android.server.appsearch.appsindexer.TestUtils.createUsageEvents;
import static com.android.server.appsearch.appsindexer.TestUtils.setupMockPackageManager;
-
import static com.google.common.truth.Truth.assertThat;
-
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageInfo;
@@ -35,24 +38,22 @@
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.util.ArrayMap;
-
import androidx.test.core.app.ApplicationProvider;
-
import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionStaticMetadata;
import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
-
import com.google.common.collect.ImmutableList;
-
+import com.google.common.collect.ImmutableMap;
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Map;
import org.junit.Test;
import org.mockito.Mockito;
-import java.io.ByteArrayInputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
/** This tests that we can convert what comes from PackageManager to a MobileApplication */
public class AppsUtilTest {
+
@Test
public void testBuildAppsFromPackageInfos_ReturnsNonNullList() throws Exception {
PackageManager pm = Mockito.mock(PackageManager.class);
@@ -110,6 +111,35 @@
// results in non-empty documents
@Test
+ public void testRealUsageStatsManager() {
+ UsageStatsManager mockUsageStatsManager = Mockito.mock(UsageStatsManager.class);
+
+ UsageEvents.Event[] events =
+ new UsageEvents.Event[] {
+ createIndividualUsageEvent(
+ UsageEvents.Event.MOVE_TO_FOREGROUND, 1000L, "com.example.package"),
+ createIndividualUsageEvent(
+ UsageEvents.Event.ACTIVITY_RESUMED, 2000L, "com.example.package"),
+ createIndividualUsageEvent(
+ UsageEvents.Event.MOVE_TO_FOREGROUND, 3000L, "com.example.package2"),
+ createIndividualUsageEvent(
+ UsageEvents.Event.MOVE_TO_BACKGROUND, 4000L, "com.example.package2")
+ };
+
+ UsageEvents mockUsageEvents = TestUtils.createUsageEvents(events);
+ when(mockUsageStatsManager.queryEvents(anyLong(), anyLong())).thenReturn(mockUsageEvents);
+
+ Map<String, List<Long>> appOpenTimestamps =
+ AppsUtil.getAppOpenTimestamps(
+ mockUsageStatsManager, 0, Calendar.getInstance().getTimeInMillis());
+
+ assertThat(appOpenTimestamps)
+ .containsExactly(
+ "com.example.package", List.of(1000L, 2000L),
+ "com.example.package2", List.of(3000L));
+ }
+
+ @Test
public void testRetrieveAppFunctionResolveInfo() throws Exception {
// Set up fake PackageManager with 10 Packages and 10 AppFunctions
PackageManager pm = Mockito.mock(PackageManager.class);
diff --git a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/TestUtils.java b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/TestUtils.java
index 7d7e957..417cc34 100644
--- a/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/TestUtils.java
+++ b/testing/appsindexertests/src/com/android/server/appsearch/appsindexer/TestUtils.java
@@ -17,7 +17,6 @@
package com.android.server.appsearch.appsindexer;
import static com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication.SCHEMA_TYPE;
-
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -36,6 +35,7 @@
import android.app.appsearch.SetSchemaResponse;
import android.app.appsearch.testutil.AppSearchSessionShimImpl;
import android.app.appsearch.testutil.GlobalSearchSessionShimImpl;
+import android.app.usage.UsageEvents;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
@@ -46,18 +46,16 @@
import android.content.pm.Signature;
import android.content.pm.SigningInfo;
import android.content.res.Resources;
-
import com.android.server.appsearch.appsindexer.appsearchtypes.AppFunctionStaticMetadata;
import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication;
-
-import org.mockito.Mockito;
-
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
+import org.mockito.Mockito;
class TestUtils {
// In the mocking tests, integers are appended to this prefix to create unique package names.
@@ -68,15 +66,18 @@
// upgrades. It is compatible as changing to MobileApplication just adds properties.
public static final AppSearchSchema COMPATIBLE_APP_SCHEMA =
new AppSearchSchema.Builder(SCHEMA_TYPE)
- .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
- MobileApplication.APP_PROPERTY_PACKAGE_NAME)
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(
- AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .setTokenizerType(
- AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_VERBATIM)
- .build())
+ .addProperty(
+ new AppSearchSchema.StringPropertyConfig.Builder(
+ MobileApplication.APP_PROPERTY_PACKAGE_NAME)
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig
+ .INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(
+ AppSearchSchema.StringPropertyConfig
+ .TOKENIZER_TYPE_VERBATIM)
+ .build())
.build();
// Represents a schema incompatible with MobileApplication. This is used to test incompatible
@@ -84,21 +85,24 @@
// "NotPackageName" field.
public static final AppSearchSchema INCOMPATIBLE_APP_SCHEMA =
new AppSearchSchema.Builder(SCHEMA_TYPE)
- .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("NotPackageName")
- .setCardinality(
- AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
- .setIndexingType(
- AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
- .setTokenizerType(
- AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
- .build())
+ .addProperty(
+ new AppSearchSchema.StringPropertyConfig.Builder("NotPackageName")
+ .setCardinality(
+ AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setIndexingType(
+ AppSearchSchema.StringPropertyConfig
+ .INDEXING_TYPE_PREFIXES)
+ .setTokenizerType(
+ AppSearchSchema.StringPropertyConfig
+ .TOKENIZER_TYPE_PLAIN)
+ .build())
.build();
/**
* Creates a fake {@link PackageInfo} object.
*
* @param variant provides variation in the mocked PackageInfo so we can index multiple fake
- * apps.
+ * apps.
*/
@NonNull
public static PackageInfo createFakePackageInfo(int variant) {
@@ -235,9 +239,9 @@
/**
* Search for documents indexed by the Apps Indexer. The database, namespace, and schematype are
* all configured.
+ *
* @param pageSize The page size to use in the {@link SearchSpec}. By setting to a expected
- * amount + 1, you can verify that the expected quantity of apps docs are
- * present.
+ * amount + 1, you can verify that the expected quantity of apps docs are present.
*/
@NonNull
public static List<SearchResult> searchAppSearchForApps(int pageSize)
@@ -258,7 +262,7 @@
.build();
// Don't want to get this confused with real indexed apps.
SearchResultsShim results =
- globalSession.search(/*queryExpression=*/ "com.fake.package", allDocumentIdsSpec);
+ globalSession.search(/* queryExpression= */ "com.fake.package", allDocumentIdsSpec);
return results.getNextPageAsync().get();
}
@@ -349,5 +353,32 @@
}
return packageIdList;
}
-}
+ /**
+ * Creates a mock {@link UsageEvents} object.
+ *
+ * @param events the events to add to the UsageEvents object.
+ * @return a {@link UsageEvents} object with the given events.
+ */
+ public static UsageEvents createUsageEvents(UsageEvents.Event... events) {
+ return new UsageEvents(Arrays.asList(events), new String[] {});
+ }
+
+ /**
+ * Creates a mock {@link UsageEvents.Event} object.
+ *
+ * @param eventType the event type of the UsageEvents.Event object.
+ * @param timestamp the timestamp of the UsageEvents.Event object.
+ * @param packageName the package name of the UsageEvents.Event object.
+ * @return a {@link UsageEvents.Event} object with the given event type, timestamp, and package
+ * name.
+ */
+ public static UsageEvents.Event createIndividualUsageEvent(
+ int eventType, long timestamp, String packageName) {
+ UsageEvents.Event e = new UsageEvents.Event();
+ e.mEventType = eventType;
+ e.mTimeStamp = timestamp;
+ e.mPackage = packageName;
+ return e;
+ }
+}