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;
+    }
+}