[pm] Implement the BIC service

The BIC service monitors two categories of events:

1. usage events to tell the foreground/background state
of package installers.
2. packages install/uninstall.

Based on the timestamps of the above events, the BIC service
detects the background installed packages.
The BIC service also stores the list of background installed
packages on the disk.

The clients of the BIC service can query the list of background
installed packages.

Bug: 238451991
Test: BackgroundInstallControlServiceTest
Change-Id: I58c98a176897893b60cf24d01c69266771d102ca
diff --git a/core/proto/android/server/background_install_control.proto b/core/proto/android/server/background_install_control.proto
new file mode 100644
index 0000000..38e6b4d
--- /dev/null
+++ b/core/proto/android/server/background_install_control.proto
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+syntax = "proto2";
+package com.android.server.pm;
+
+option java_multiple_files = true;
+
+// Proto for the background installed packages.
+// It's used for serializing the background installed package info to disk.
+message BackgroundInstalledPackagesProto {
+  repeated BackgroundInstalledPackageProto bg_installed_pkg = 1;
+}
+
+// Proto for the background installed package entry
+message BackgroundInstalledPackageProto {
+  optional string package_name = 1;
+  optional int32 user_id = 2;
+}
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index df95f86..d4c4c69 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -17,19 +17,46 @@
 package com.android.server.pm;
 
 import android.annotation.NonNull;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManagerInternal;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.IBackgroundInstallControlService;
 import android.content.pm.IPackageManager;
+import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.ParceledListSlice;
-import android.os.IBinder;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.Slog;
 import android.util.SparseArrayMap;
+import android.util.SparseSetArray;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
 import com.android.server.SystemService;
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ListIterator;
+import java.util.Set;
+import java.util.TreeSet;
 
 /**
  * @hide
@@ -37,14 +64,30 @@
 public class BackgroundInstallControlService extends SystemService {
     private static final String TAG = "BackgroundInstallControlService";
 
+    private static final String DISK_FILE_NAME = "states";
+    private static final String DISK_DIR_NAME = "bic";
+
+    private static final int MAX_FOREGROUND_TIME_FRAMES_SIZE = 10;
+
+    private static final int MSG_USAGE_EVENT_RECEIVED = 0;
+    private static final int MSG_PACKAGE_ADDED = 1;
+    private static final int MSG_PACKAGE_REMOVED = 2;
+
     private final Context mContext;
     private final BinderService mBinderService;
     private final IPackageManager mIPackageManager;
+    private final PackageManagerInternal mPackageManagerInternal;
+    private final UsageStatsManagerInternal mUsageStatsManagerInternal;
+    private final PermissionManagerServiceInternal mPermissionManager;
+    private final Handler mHandler;
+    private final File mDiskFile;
 
-    // User ID -> package name -> time diff
-    // The time diff between the last foreground activity installer and
-    // the "onPackageAdded" function call.
-    private final SparseArrayMap<String, Long> mBackgroundInstalledPackages =
+
+    private SparseSetArray<String> mBackgroundInstalledPackages = null;
+
+    // User ID -> package name -> set of foreground time frame
+    private final SparseArrayMap<String,
+            TreeSet<ForegroundTimeFrame>> mInstallerForegroundTimeFrames =
             new SparseArrayMap<>();
 
     public BackgroundInstallControlService(@NonNull Context context) {
@@ -56,49 +99,385 @@
         super(injector.getContext());
         mContext = injector.getContext();
         mIPackageManager = injector.getIPackageManager();
+        mPackageManagerInternal = injector.getPackageManagerInternal();
+        mPermissionManager = injector.getPermissionManager();
+        mHandler = new EventHandler(injector.getLooper(), this);
+        mDiskFile = injector.getDiskFile();
+        mUsageStatsManagerInternal = injector.getUsageStatsManagerInternal();
+        mUsageStatsManagerInternal.registerListener(
+                (userId, event) ->
+                        mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED,
+                                userId,
+                                0,
+                                event).sendToTarget()
+        );
         mBinderService = new BinderService(this);
     }
 
     private static final class BinderService extends IBackgroundInstallControlService.Stub {
         final BackgroundInstallControlService mService;
 
-        BinderService(BackgroundInstallControlService service)  {
+        BinderService(BackgroundInstallControlService service) {
             mService = service;
         }
 
         @Override
         public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
                 @PackageManager.PackageInfoFlagsBits long flags, int userId) {
-            ParceledListSlice<PackageInfo> packages;
-            try {
-                packages = mService.mIPackageManager.getInstalledPackages(flags, userId);
-            } catch (RemoteException e) {
-                throw new IllegalStateException("Package manager not available", e);
-            }
-
-            // TODO(b/244216300): to enable the test the usage by BinaryTransparencyService,
-            // we currently comment out the actual implementation.
-            // The fake implementation is just to filter out the first app of the list.
-            // for (int i = 0, size = packages.getList().size(); i < size; i++) {
-            //     String packageName = packages.getList().get(i).packageName;
-            //     if (!mBackgroundInstalledPackages.contains(userId, packageName) {
-            //         packages.getList().remove(i);
-            //     }
-            // }
-            if (packages.getList().size() > 0) {
-                packages.getList().remove(0);
-            }
-            return packages;
+            return mService.getBackgroundInstalledPackages(flags, userId);
         }
     }
 
-    /**
-     * Called when the system service should publish a binder service using
-     * {@link #publishBinderService(String, IBinder).}
-     */
+    @VisibleForTesting
+    ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
+            @PackageManager.PackageInfoFlagsBits long flags, int userId) {
+        ParceledListSlice<PackageInfo> packages;
+        try {
+            packages = mIPackageManager.getInstalledPackages(flags, userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        initBackgroundInstalledPackages();
+
+        ListIterator<PackageInfo> iter = packages.getList().listIterator();
+        while (iter.hasNext()) {
+            String packageName = iter.next().packageName;
+            if (!mBackgroundInstalledPackages.contains(userId, packageName)) {
+                iter.remove();
+            }
+        }
+
+        return packages;
+    }
+
+    private static class EventHandler extends Handler {
+        private final BackgroundInstallControlService mService;
+
+        EventHandler(Looper looper, BackgroundInstallControlService service) {
+            super(looper);
+            mService = service;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_USAGE_EVENT_RECEIVED: {
+                    mService.handleUsageEvent((UsageEvents.Event) msg.obj, msg.arg1 /* userId */);
+                    break;
+                }
+                case MSG_PACKAGE_ADDED: {
+                    mService.handlePackageAdd((String) msg.obj, msg.arg1 /* userId */);
+                    break;
+                }
+                case MSG_PACKAGE_REMOVED: {
+                    mService.handlePackageRemove((String) msg.obj, msg.arg1 /* userId */);
+                    break;
+                }
+                default:
+                    Slog.w(TAG, "Unknown message: " + msg.what);
+            }
+        }
+    }
+
+    void handlePackageAdd(String packageName, int userId) {
+        InstallSourceInfo installSourceInfo = null;
+        try {
+            installSourceInfo = mIPackageManager.getInstallSourceInfo(packageName);
+        } catch (RemoteException e) {
+            // Failed to talk to PackageManagerService Should never happen!
+            throw e.rethrowFromSystemServer();
+        }
+        String installerPackageName =
+                installSourceInfo == null ? null : installSourceInfo.getInstallingPackageName();
+        if (installerPackageName == null) {
+            Slog.w(TAG, "fails to get installerPackageName for " + packageName);
+            return;
+        }
+
+        ApplicationInfo appInfo = null;
+        try {
+            appInfo = mIPackageManager.getApplicationInfo(packageName,
+                    0, userId);
+        } catch (RemoteException e) {
+            // Failed to talk to PackageManagerService Should never happen!
+            throw e.rethrowFromSystemServer();
+        }
+
+        if (appInfo == null) {
+            Slog.w(TAG, "fails to get appInfo for " + packageName);
+            return;
+        }
+
+        // convert up-time to current time.
+        final long installTimestamp = System.currentTimeMillis()
+                - (SystemClock.uptimeMillis() - appInfo.createTimestamp);
+
+        if (wasForegroundInstallation(installerPackageName, userId, installTimestamp)) {
+            return;
+        }
+
+        initBackgroundInstalledPackages();
+        mBackgroundInstalledPackages.add(userId, packageName);
+        writeBackgroundInstalledPackagesToDisk();
+    }
+
+    private boolean wasForegroundInstallation(String installerPackageName,
+            int userId, long installTimestamp) {
+        TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames =
+                mInstallerForegroundTimeFrames.get(userId, installerPackageName);
+
+        // The installer never run in foreground.
+        if (foregroundTimeFrames == null) {
+            return false;
+        }
+
+        for (var foregroundTimeFrame : foregroundTimeFrames) {
+            // the foreground time frame starts later than the installation.
+            // so the installation is outside the foreground time frame.
+            if (foregroundTimeFrame.startTimeStampMillis > installTimestamp) {
+                continue;
+            }
+
+            // the foreground time frame is not over yet.
+            // the installation is inside the foreground time frame.
+            if (!foregroundTimeFrame.isDone()) {
+                return true;
+            }
+
+            // the foreground time frame ends later than the installation.
+            // the installation is inside the foreground time frame.
+            if (installTimestamp <= foregroundTimeFrame.endTimeStampMillis) {
+                return true;
+            }
+        }
+
+        // the installation is not inside any of foreground time frames.
+        // so it is not a foreground installation.
+        return false;
+    }
+
+    void handlePackageRemove(String packageName, int userId) {
+        initBackgroundInstalledPackages();
+        mBackgroundInstalledPackages.remove(userId, packageName);
+        writeBackgroundInstalledPackagesToDisk();
+    }
+
+    void handleUsageEvent(UsageEvents.Event event, int userId) {
+        if (event.mEventType != UsageEvents.Event.ACTIVITY_RESUMED
+                && event.mEventType != UsageEvents.Event.ACTIVITY_PAUSED
+                && event.mEventType != UsageEvents.Event.ACTIVITY_STOPPED) {
+            return;
+        }
+
+        if (!isInstaller(event.mPackage, userId)) {
+            return;
+        }
+
+        if (!mInstallerForegroundTimeFrames.contains(userId, event.mPackage)) {
+            mInstallerForegroundTimeFrames.add(userId, event.mPackage, new TreeSet<>());
+        }
+
+        TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames =
+                mInstallerForegroundTimeFrames.get(userId, event.mPackage);
+
+        if ((foregroundTimeFrames.size() == 0) || foregroundTimeFrames.last().isDone()) {
+            // ignore the other events if there is no open ForegroundTimeFrame.
+            if (event.mEventType != UsageEvents.Event.ACTIVITY_RESUMED) {
+                return;
+            }
+            foregroundTimeFrames.add(new ForegroundTimeFrame(event.mTimeStamp));
+        }
+
+        foregroundTimeFrames.last().addEvent(event);
+
+        if (foregroundTimeFrames.size() > MAX_FOREGROUND_TIME_FRAMES_SIZE) {
+            foregroundTimeFrames.pollFirst();
+        }
+    }
+
+    @VisibleForTesting
+    void writeBackgroundInstalledPackagesToDisk() {
+        AtomicFile atomicFile = new AtomicFile(mDiskFile);
+        FileOutputStream fileOutputStream;
+        try {
+            fileOutputStream = atomicFile.startWrite();
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to start write to states protobuf.", e);
+            return;
+        }
+
+        try {
+            ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
+            for (int i = 0; i < mBackgroundInstalledPackages.size(); i++) {
+                int userId = mBackgroundInstalledPackages.keyAt(i);
+                for (String packageName : mBackgroundInstalledPackages.get(userId)) {
+                    long token = protoOutputStream.start(
+                            BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                    protoOutputStream.write(
+                            BackgroundInstalledPackageProto.PACKAGE_NAME, packageName);
+                    protoOutputStream.write(
+                            BackgroundInstalledPackageProto.USER_ID, userId + 1);
+                    protoOutputStream.end(token);
+                }
+            }
+            protoOutputStream.flush();
+            atomicFile.finishWrite(fileOutputStream);
+        } catch (Exception e) {
+            Slog.e(TAG, "Failed to finish write to states protobuf.", e);
+            atomicFile.failWrite(fileOutputStream);
+        }
+    }
+
+    @VisibleForTesting
+    void initBackgroundInstalledPackages() {
+        if (mBackgroundInstalledPackages != null) {
+            return;
+        }
+
+        mBackgroundInstalledPackages = new SparseSetArray<>();
+
+        if (!mDiskFile.exists()) {
+            return;
+        }
+
+        AtomicFile atomicFile = new AtomicFile(mDiskFile);
+        try (FileInputStream fileInputStream = atomicFile.openRead()) {
+            ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
+
+            while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (protoInputStream.getFieldNumber()
+                        != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
+                    continue;
+                }
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                String packageName = null;
+                int userId = UserHandle.USER_NULL;
+                while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (protoInputStream.getFieldNumber()) {
+                        case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            break;
+                        case (int) BackgroundInstalledPackageProto.USER_ID:
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            break;
+                        default:
+                            Slog.w(TAG, "Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
+                    }
+                }
+                protoInputStream.end(token);
+                if (packageName != null && userId != UserHandle.USER_NULL) {
+                    mBackgroundInstalledPackages.add(userId, packageName);
+                } else {
+                    Slog.w(TAG, "Fails to get packageName or UserId from proto file");
+                }
+            }
+        } catch (IOException e) {
+            Slog.w(TAG, "Error reading state from the disk", e);
+        }
+    }
+
+    @VisibleForTesting
+    SparseSetArray<String> getBackgroundInstalledPackages() {
+        return mBackgroundInstalledPackages;
+    }
+
+    @VisibleForTesting
+    SparseArrayMap<String, TreeSet<ForegroundTimeFrame>> getInstallerForegroundTimeFrames() {
+        return mInstallerForegroundTimeFrames;
+    }
+
+    private boolean isInstaller(String pkgName, int userId) {
+        if (mInstallerForegroundTimeFrames.contains(userId, pkgName)) {
+            return true;
+        }
+        return mPermissionManager.checkPermission(pkgName,
+                android.Manifest.permission.INSTALL_PACKAGES,
+                userId) == PackageManager.PERMISSION_GRANTED;
+    }
+
     @Override
     public void onStart() {
-        publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService);
+        onStart(/* isForTesting= */ false);
+    }
+
+    @VisibleForTesting
+    void onStart(boolean isForTesting) {
+        if (!isForTesting) {
+            publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService);
+        }
+
+        mPackageManagerInternal.getPackageList(new PackageManagerInternal.PackageListObserver() {
+            @Override
+            public void onPackageAdded(String packageName, int uid) {
+                final int userId = UserHandle.getUserId(uid);
+                mHandler.obtainMessage(MSG_PACKAGE_ADDED,
+                        userId, 0, packageName).sendToTarget();
+            }
+
+            @Override
+            public void onPackageRemoved(String packageName, int uid) {
+                final int userId = UserHandle.getUserId(uid);
+                mHandler.obtainMessage(MSG_PACKAGE_REMOVED,
+                        userId, 0, packageName).sendToTarget();
+            }
+        });
+    }
+
+    // The foreground time frame (ForegroundTimeFrame) represents the period
+    // when a package's activities continuously occupy the foreground.
+    // Each ForegroundTimeFrame starts with an ACTIVITY_RESUMED event,
+    // and then ends with an ACTIVITY_PAUSED or ACTIVITY_STOPPED event.
+    // The startTimeStampMillis stores the timestamp of the ACTIVITY_RESUMED event.
+    // The endTimeStampMillis stores the timestamp of the ACTIVITY_PAUSED or ACTIVITY_STOPPED event
+    // that wraps up the ForegroundTimeFrame.
+    // The activities are designed to handle the edge case in which a package's one activity
+    // seamlessly replace another activity of the same package. Thus, we count these activities
+    // together as a ForegroundTimeFrame. For this scenario, only when all the activities terminate
+    // shall consider the completion of the ForegroundTimeFrame.
+    static final class ForegroundTimeFrame implements Comparable<ForegroundTimeFrame> {
+        public final long startTimeStampMillis;
+        public long endTimeStampMillis;
+        public final Set<Integer> activities;
+
+        public int compareTo(ForegroundTimeFrame o) {
+            int comp = Long.compare(startTimeStampMillis, o.startTimeStampMillis);
+            if (comp != 0) return comp;
+
+            return Integer.compare(hashCode(), o.hashCode());
+        }
+
+        ForegroundTimeFrame(long startTimeStampMillis) {
+            this.startTimeStampMillis = startTimeStampMillis;
+            endTimeStampMillis = 0;
+            activities = new ArraySet<>();
+        }
+
+        public boolean isDone() {
+            return endTimeStampMillis != 0;
+        }
+
+        public void addEvent(UsageEvents.Event event) {
+            switch (event.mEventType) {
+                case UsageEvents.Event.ACTIVITY_RESUMED:
+                    activities.add(event.mInstanceId);
+                    break;
+                case UsageEvents.Event.ACTIVITY_PAUSED:
+                case UsageEvents.Event.ACTIVITY_STOPPED:
+                    if (activities.contains(event.mInstanceId)) {
+                        activities.remove(event.mInstanceId);
+                        if (activities.size() == 0) {
+                            endTimeStampMillis = event.mTimeStamp;
+                        }
+                    }
+                    break;
+                default:
+            }
+        }
     }
 
     /**
@@ -108,6 +487,16 @@
         Context getContext();
 
         IPackageManager getIPackageManager();
+
+        PackageManagerInternal getPackageManagerInternal();
+
+        UsageStatsManagerInternal getUsageStatsManagerInternal();
+
+        PermissionManagerServiceInternal getPermissionManager();
+
+        Looper getLooper();
+
+        File getDiskFile();
     }
 
     private static final class InjectorImpl implements Injector {
@@ -126,5 +515,36 @@
         public IPackageManager getIPackageManager() {
             return IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
         }
+
+        @Override
+        public PackageManagerInternal getPackageManagerInternal() {
+            return LocalServices.getService(PackageManagerInternal.class);
+        }
+
+        @Override
+        public UsageStatsManagerInternal getUsageStatsManagerInternal() {
+            return LocalServices.getService(UsageStatsManagerInternal.class);
+        }
+
+        @Override
+        public PermissionManagerServiceInternal getPermissionManager() {
+            return LocalServices.getService(PermissionManagerServiceInternal.class);
+        }
+
+        @Override
+        public Looper getLooper() {
+            ServiceThread serviceThread = new ServiceThread(TAG,
+                    android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
+            serviceThread.start();
+            return serviceThread.getLooper();
+
+        }
+
+        @Override
+        public File getDiskFile() {
+            File dir = new File(Environment.getDataSystemDirectory(), DISK_DIR_NAME);
+            File file = new File(dir, DISK_FILE_NAME);
+            return file;
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
new file mode 100644
index 0000000..54fa272
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
@@ -0,0 +1,840 @@
+/*
+ * Copyright (C) 2022 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.server.pm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageEvents.Event;
+import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.UsageEventListener;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.InstallSourceInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ParceledListSlice;
+import android.os.FileUtils;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
+import android.util.AtomicFile;
+import android.util.SparseSetArray;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.internal.util.reflection.FieldSetter;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link com.android.server.pm.BackgroundInstallControlService}
+ */
+@Presubmit
+public final class BackgroundInstallControlServiceTest {
+    private static final String INSTALLER_NAME_1 = "installer1";
+    private static final String INSTALLER_NAME_2 = "installer2";
+    private static final String PACKAGE_NAME_1 = "package1";
+    private static final String PACKAGE_NAME_2 = "package2";
+    private static final String PACKAGE_NAME_3 = "package3";
+    private static final int USER_ID_1 = 1;
+    private static final int USER_ID_2 = 2;
+    private static final long USAGE_EVENT_TIMESTAMP_1 = 1000;
+    private static final long USAGE_EVENT_TIMESTAMP_2 = 2000;
+    private static final long USAGE_EVENT_TIMESTAMP_3 = 3000;
+    private static final long PACKAGE_ADD_TIMESTAMP_1 = 1500;
+
+    private BackgroundInstallControlService mBackgroundInstallControlService;
+    private PackageManagerInternal.PackageListObserver mPackageListObserver;
+    private UsageEventListener mUsageEventListener;
+    private TestLooper mTestLooper;
+    private Looper mLooper;
+    private File mFile;
+
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private IPackageManager mIPackageManager;
+    @Mock
+    private PackageManagerInternal mPackageManagerInternal;
+    @Mock
+    private UsageStatsManagerInternal mUsageStatsManagerInternal;
+    @Mock
+    private PermissionManagerServiceInternal mPermissionManager;
+    @Captor
+    private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor;
+    @Captor
+    private ArgumentCaptor<UsageEventListener> mUsageEventListenerCaptor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mTestLooper = new TestLooper();
+        mLooper = mTestLooper.getLooper();
+        mFile = new File(
+                InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(),
+                "test");
+        mBackgroundInstallControlService = new BackgroundInstallControlService(
+                new MockInjector(mContext));
+
+        verify(mUsageStatsManagerInternal).registerListener(mUsageEventListenerCaptor.capture());
+        mUsageEventListener = mUsageEventListenerCaptor.getValue();
+
+        mBackgroundInstallControlService.onStart(true);
+        verify(mPackageManagerInternal).getPackageList(mPackageListObserverCaptor.capture());
+        mPackageListObserver = mPackageListObserverCaptor.getValue();
+    }
+
+    @After
+    public void tearDown() {
+        FileUtils.deleteContentsAndDir(mFile);
+    }
+
+    @Test
+    public void testInitBackgroundInstalledPackages_empty() {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        assertNotNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        assertEquals(0,
+                mBackgroundInstallControlService.getBackgroundInstalledPackages().size());
+    }
+
+    @Test
+    public void testInitBackgroundInstalledPackages_one() {
+        AtomicFile atomicFile = new AtomicFile(mFile);
+        FileOutputStream fileOutputStream;
+        try {
+            fileOutputStream = atomicFile.startWrite();
+        } catch (IOException e) {
+            fail("Failed to start write to states protobuf." + e);
+            return;
+        }
+
+        // Write test data to the file on the disk.
+        try {
+            ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
+            long token = protoOutputStream.start(
+                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
+            protoOutputStream.end(token);
+            protoOutputStream.flush();
+            atomicFile.finishWrite(fileOutputStream);
+        } catch (Exception e) {
+            fail("Failed to finish write to states protobuf. " + e);
+            atomicFile.failWrite(fileOutputStream);
+        }
+
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(1, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+
+    @Test
+    public void testInitBackgroundInstalledPackages_two() {
+        AtomicFile atomicFile = new AtomicFile(mFile);
+        FileOutputStream fileOutputStream;
+        try {
+            fileOutputStream = atomicFile.startWrite();
+        } catch (IOException e) {
+            fail("Failed to start write to states protobuf." + e);
+            return;
+        }
+
+        // Write test data to the file on the disk.
+        try {
+            ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
+
+            long token = protoOutputStream.start(
+                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1);
+            protoOutputStream.end(token);
+
+            token = protoOutputStream.start(
+                    BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_2);
+            protoOutputStream.write(
+                    BackgroundInstalledPackageProto.USER_ID, USER_ID_2 + 1);
+            protoOutputStream.end(token);
+
+            protoOutputStream.flush();
+            atomicFile.finishWrite(fileOutputStream);
+        } catch (Exception e) {
+            fail("Failed to finish write to states protobuf. " + e);
+            atomicFile.failWrite(fileOutputStream);
+        }
+
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(2, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+        assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2));
+    }
+
+    @Test
+    public void testWriteBackgroundInstalledPackagesToDisk_empty() {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        mBackgroundInstallControlService.writeBackgroundInstalledPackagesToDisk();
+
+        // Read the file on the disk to verify
+        var packagesInDisk = new SparseSetArray<>();
+        AtomicFile atomicFile = new AtomicFile(mFile);
+        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
+            ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
+
+            while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (protoInputStream.getFieldNumber()
+                        != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
+                    continue;
+                }
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                String packageName = null;
+                int userId = UserHandle.USER_NULL;
+                while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (protoInputStream.getFieldNumber()) {
+                        case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            break;
+                        case (int) BackgroundInstalledPackageProto.USER_ID:
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            break;
+                        default:
+                            fail("Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
+                    }
+                }
+                protoInputStream.end(token);
+                if (packageName != null && userId != UserHandle.USER_NULL) {
+                    packagesInDisk.add(userId, packageName);
+                } else {
+                    fail("Fails to get packageName or UserId from proto file");
+                }
+            }
+        } catch (IOException e) {
+            fail("Error reading state from the disk. " + e);
+        }
+
+        assertEquals(0, packagesInDisk.size());
+        assertEquals(packages.size(), packagesInDisk.size());
+    }
+
+    @Test
+    public void testWriteBackgroundInstalledPackagesToDisk_one() {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+
+        packages.add(USER_ID_1, PACKAGE_NAME_1);
+        mBackgroundInstallControlService.writeBackgroundInstalledPackagesToDisk();
+
+        // Read the file on the disk to verify
+        var packagesInDisk = new SparseSetArray<>();
+        AtomicFile atomicFile = new AtomicFile(mFile);
+        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
+            ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
+
+            while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (protoInputStream.getFieldNumber()
+                        != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
+                    continue;
+                }
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                String packageName = null;
+                int userId = UserHandle.USER_NULL;
+                while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (protoInputStream.getFieldNumber()) {
+                        case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            break;
+                        case (int) BackgroundInstalledPackageProto.USER_ID:
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            break;
+                        default:
+                            fail("Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
+                    }
+                }
+                protoInputStream.end(token);
+                if (packageName != null && userId != UserHandle.USER_NULL) {
+                    packagesInDisk.add(userId, packageName);
+                } else {
+                    fail("Fails to get packageName or UserId from proto file");
+                }
+            }
+        } catch (IOException e) {
+            fail("Error reading state from the disk. " + e);
+        }
+
+        assertEquals(1, packagesInDisk.size());
+        assertEquals(packages.size(), packagesInDisk.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+
+    @Test
+    public void testWriteBackgroundInstalledPackagesToDisk_two() {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+
+        packages.add(USER_ID_1, PACKAGE_NAME_1);
+        packages.add(USER_ID_2, PACKAGE_NAME_2);
+        mBackgroundInstallControlService.writeBackgroundInstalledPackagesToDisk();
+
+        // Read the file on the disk to verify
+        var packagesInDisk = new SparseSetArray<>();
+        AtomicFile atomicFile = new AtomicFile(mFile);
+        try (FileInputStream fileInputStream  = atomicFile.openRead()) {
+            ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
+
+            while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (protoInputStream.getFieldNumber()
+                        != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
+                    continue;
+                }
+                long token = protoInputStream.start(
+                        BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
+                String packageName = null;
+                int userId = UserHandle.USER_NULL;
+                while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (protoInputStream.getFieldNumber()) {
+                        case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
+                            packageName = protoInputStream.readString(
+                                    BackgroundInstalledPackageProto.PACKAGE_NAME);
+                            break;
+                        case (int) BackgroundInstalledPackageProto.USER_ID:
+                            userId = protoInputStream.readInt(
+                                    BackgroundInstalledPackageProto.USER_ID) - 1;
+                            break;
+                        default:
+                            fail("Undefined field in proto: "
+                                    + protoInputStream.getFieldNumber());
+                    }
+                }
+                protoInputStream.end(token);
+                if (packageName != null && userId != UserHandle.USER_NULL) {
+                    packagesInDisk.add(userId, packageName);
+                } else {
+                    fail("Fails to get packageName or UserId from proto file");
+                }
+            }
+        } catch (IOException e) {
+            fail("Error reading state from the disk. " + e);
+        }
+
+        assertEquals(2, packagesInDisk.size());
+        assertEquals(packages.size(), packagesInDisk.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+        assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2));
+    }
+
+    @Test
+    public void testHandleUsageEvent_permissionDenied() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_DENIED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, 0);
+        mTestLooper.dispatchAll();
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+    }
+
+    @Test
+    public void testHandleUsageEvent_permissionGranted() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, 0);
+        mTestLooper.dispatchAll();
+        assertEquals(1,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+    }
+
+    @Test
+    public void testHandleUsageEvent_ignoredEvent() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.USER_INTERACTION,
+                USER_ID_1, INSTALLER_NAME_1, 0);
+        mTestLooper.dispatchAll();
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+    }
+
+    @Test
+    public void testHandleUsageEvent_firstActivityResumedHalfTimeFrame() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        mTestLooper.dispatchAll();
+
+        var installerForegroundTimeFrames =
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames();
+        assertEquals(1, installerForegroundTimeFrames.numMaps());
+        assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1));
+
+        var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1);
+        assertEquals(1, foregroundTimeFrames.size());
+
+        var foregroundTimeFrame = foregroundTimeFrames.first();
+        assertEquals(USAGE_EVENT_TIMESTAMP_1, foregroundTimeFrame.startTimeStampMillis);
+        assertFalse(foregroundTimeFrame.isDone());
+    }
+
+    @Test
+    public void testHandleUsageEvent_firstActivityResumedOneTimeFrame() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        mTestLooper.dispatchAll();
+
+        var installerForegroundTimeFrames =
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames();
+        assertEquals(1, installerForegroundTimeFrames.numMaps());
+        assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1));
+
+        var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1);
+        assertEquals(1, foregroundTimeFrames.size());
+
+        var foregroundTimeFrame = foregroundTimeFrames.first();
+        assertEquals(USAGE_EVENT_TIMESTAMP_1, foregroundTimeFrame.startTimeStampMillis);
+        assertTrue(foregroundTimeFrame.isDone());
+    }
+
+    @Test
+    public void testHandleUsageEvent_firstActivityResumedOneAndHalfTimeFrame() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+        mTestLooper.dispatchAll();
+
+        var installerForegroundTimeFrames =
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames();
+        assertEquals(1, installerForegroundTimeFrames.numMaps());
+        assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1));
+
+        var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1);
+        assertEquals(2, foregroundTimeFrames.size());
+
+        var foregroundTimeFrame1 = foregroundTimeFrames.first();
+        assertEquals(USAGE_EVENT_TIMESTAMP_1, foregroundTimeFrame1.startTimeStampMillis);
+        assertTrue(foregroundTimeFrame1.isDone());
+
+        var foregroundTimeFrame2 = foregroundTimeFrames.last();
+        assertEquals(USAGE_EVENT_TIMESTAMP_3, foregroundTimeFrame2.startTimeStampMillis);
+        assertFalse(foregroundTimeFrame2.isDone());
+    }
+
+    @Test
+    public void testHandleUsageEvent_firstNoneActivityResumed() {
+        assertEquals(0,
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps());
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        mTestLooper.dispatchAll();
+
+        var installerForegroundTimeFrames =
+                mBackgroundInstallControlService.getInstallerForegroundTimeFrames();
+        assertEquals(1, installerForegroundTimeFrames.numMaps());
+        assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1));
+
+        var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1);
+        assertEquals(0, foregroundTimeFrames.size());
+    }
+
+    @Test
+    public void testHandleUsageEvent_packageAddedNoUsageEvent() throws
+            RemoteException, NoSuchFieldException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
+        assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
+        when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+        ApplicationInfo appInfo = mock(ApplicationInfo.class);
+
+        when(mIPackageManager.getApplicationInfo(
+                eq(PACKAGE_NAME_1),
+                eq(0L),
+                anyInt())
+        ).thenReturn(appInfo);
+
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
+                ApplicationInfo.class.getDeclaredField("createTimestamp"),
+                createTimestamp);
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(1, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+
+    @Test
+    public void testHandleUsageEvent_packageAddedInsideTimeFrame() throws
+            RemoteException, NoSuchFieldException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
+        assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
+        when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+        ApplicationInfo appInfo = mock(ApplicationInfo.class);
+
+        when(mIPackageManager.getApplicationInfo(
+                eq(PACKAGE_NAME_1),
+                eq(0L),
+                anyInt())
+        ).thenReturn(appInfo);
+
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
+                ApplicationInfo.class.getDeclaredField("createTimestamp"),
+                createTimestamp);
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        // The following 2 usage events generation is the only difference from the
+        // testHandleUsageEvent_packageAddedNoUsageEvent test.
+        // The 2 usage events make the package adding inside a time frame.
+        // So it's not a background install. Thus, it's null for the return of
+        // mBackgroundInstallControlService.getBackgroundInstalledPackages()
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+
+        mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+    }
+
+    @Test
+    public void testHandleUsageEvent_packageAddedOutsideTimeFrame1() throws
+            RemoteException, NoSuchFieldException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
+        assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
+        when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+        ApplicationInfo appInfo = mock(ApplicationInfo.class);
+
+        when(mIPackageManager.getApplicationInfo(
+                eq(PACKAGE_NAME_1),
+                eq(0L),
+                anyInt())
+        ).thenReturn(appInfo);
+
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
+                ApplicationInfo.class.getDeclaredField("createTimestamp"),
+                createTimestamp);
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        // The following 2 usage events generation is the only difference from the
+        // testHandleUsageEvent_packageAddedNoUsageEvent test.
+        // The 2 usage events make the package adding outside a time frame.
+        // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame,
+        // it's a background install. Thus, it's not null for the return of
+        // mBackgroundInstallControlService.getBackgroundInstalledPackages()
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3);
+
+        mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(1, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+    @Test
+    public void testHandleUsageEvent_packageAddedOutsideTimeFrame2() throws
+            RemoteException, NoSuchFieldException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        InstallSourceInfo installSourceInfo = new InstallSourceInfo(
+                /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null,
+                /* originatingPackageName = */ null,
+                /* installingPackageName = */ INSTALLER_NAME_1);
+        assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1);
+        when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo);
+        ApplicationInfo appInfo = mock(ApplicationInfo.class);
+
+        when(mIPackageManager.getApplicationInfo(
+                eq(PACKAGE_NAME_1),
+                eq(0L),
+                anyInt())
+        ).thenReturn(appInfo);
+
+        long createTimestamp = PACKAGE_ADD_TIMESTAMP_1
+                - (System.currentTimeMillis() - SystemClock.uptimeMillis());
+        FieldSetter.setField(appInfo,
+                ApplicationInfo.class.getDeclaredField("createTimestamp"),
+                createTimestamp);
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        // The following 2 usage events generation is the only difference from the
+        // testHandleUsageEvent_packageAddedNoUsageEvent test.
+        // These 2 usage events are triggered by INSTALLER_NAME_2.
+        // The 2 usage events make the package adding outside a time frame.
+        // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame,
+        // it's a background install. Thus, it's not null for the return of
+        // mBackgroundInstallControlService.getBackgroundInstalledPackages()
+        doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission(
+                anyString(), anyString(), anyInt());
+        generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED,
+                USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_2);
+        generateUsageEvent(Event.ACTIVITY_STOPPED,
+                USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_3);
+
+        mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+        assertEquals(1, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+    }
+
+    @Test
+    public void testPackageRemoved() {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(packages);
+
+        packages.add(USER_ID_1, PACKAGE_NAME_1);
+        packages.add(USER_ID_2, PACKAGE_NAME_2);
+
+        assertEquals(2, packages.size());
+        assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+        assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2));
+
+        int uid = USER_ID_1 * UserHandle.PER_USER_RANGE;
+        assertEquals(USER_ID_1, UserHandle.getUserId(uid));
+
+        mPackageListObserver.onPackageRemoved(PACKAGE_NAME_1, uid);
+        mTestLooper.dispatchAll();
+
+        assertEquals(1, packages.size());
+        assertFalse(packages.contains(USER_ID_1, PACKAGE_NAME_1));
+        assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2));
+    }
+
+    @Test
+    public void testGetBackgroundInstalledPackages() throws RemoteException {
+        assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages());
+        mBackgroundInstallControlService.initBackgroundInstalledPackages();
+        var bgPackages = mBackgroundInstallControlService.getBackgroundInstalledPackages();
+        assertNotNull(bgPackages);
+
+        bgPackages.add(USER_ID_1, PACKAGE_NAME_1);
+        bgPackages.add(USER_ID_2, PACKAGE_NAME_2);
+
+        assertEquals(2, bgPackages.size());
+        assertTrue(bgPackages.contains(USER_ID_1, PACKAGE_NAME_1));
+        assertTrue(bgPackages.contains(USER_ID_2, PACKAGE_NAME_2));
+
+        List<PackageInfo> packages = new ArrayList<>();
+        var packageInfo1 = makePackageInfo(PACKAGE_NAME_1);
+        packages.add(packageInfo1);
+        var packageInfo2 = makePackageInfo(PACKAGE_NAME_2);
+        packages.add(packageInfo2);
+        var packageInfo3 = makePackageInfo(PACKAGE_NAME_3);
+        packages.add(packageInfo3);
+        doReturn(new ParceledListSlice<>(packages)).when(mIPackageManager).getInstalledPackages(
+                anyLong(), anyInt());
+
+        var resultPackages =
+                mBackgroundInstallControlService.getBackgroundInstalledPackages(0L, USER_ID_1);
+        assertEquals(1, resultPackages.getList().size());
+        assertTrue(resultPackages.getList().contains(packageInfo1));
+        assertFalse(resultPackages.getList().contains(packageInfo2));
+        assertFalse(resultPackages.getList().contains(packageInfo3));
+    }
+
+    /**
+     * Mock a usage event occurring.
+     *
+     * @param usageEventId id of a usage event
+     * @param userId user id of a usage event
+     * @param pkgName package name of a usage event
+     * @param timestamp timestamp of a usage event
+     */
+    private void generateUsageEvent(int usageEventId,
+            int userId,
+            String pkgName,
+            long timestamp) {
+        Event event = new Event(usageEventId, timestamp);
+        event.mPackage = pkgName;
+        mUsageEventListener.onUsageEvent(userId, event);
+    }
+
+    private PackageInfo makePackageInfo(String packageName) {
+        PackageInfo pkg = new PackageInfo();
+        pkg.packageName = packageName;
+        pkg.applicationInfo = new ApplicationInfo();
+        return pkg;
+    }
+
+    private class MockInjector implements BackgroundInstallControlService.Injector {
+        private final Context mContext;
+
+        MockInjector(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public Context getContext() {
+            return mContext;
+        }
+
+        @Override
+        public IPackageManager getIPackageManager() {
+            return mIPackageManager;
+        }
+
+        @Override
+        public PackageManagerInternal getPackageManagerInternal() {
+            return mPackageManagerInternal;
+        }
+
+        @Override
+        public UsageStatsManagerInternal getUsageStatsManagerInternal() {
+            return mUsageStatsManagerInternal;
+        }
+
+        @Override
+        public PermissionManagerServiceInternal getPermissionManager() {
+            return mPermissionManager;
+        }
+
+        @Override
+        public Looper getLooper() {
+            return mLooper;
+        }
+
+        @Override
+        public File getDiskFile() {
+            return mFile;
+        }
+    }
+}