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 0000000000000..38e6b4d21e2d1 --- /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 df95f86d1e079..d4c4c694a6c57 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 @@ import com.android.server.SystemService; 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 mBackgroundInstalledPackages = + + private SparseSetArray mBackgroundInstalledPackages = null; + + // User ID -> package name -> set of foreground time frame + private final SparseArrayMap> mInstallerForegroundTimeFrames = new SparseArrayMap<>(); public BackgroundInstallControlService(@NonNull Context context) { @@ -56,49 +99,385 @@ public class BackgroundInstallControlService extends SystemService { 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 getBackgroundInstalledPackages( @PackageManager.PackageInfoFlagsBits long flags, int userId) { - ParceledListSlice 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 getBackgroundInstalledPackages( + @PackageManager.PackageInfoFlagsBits long flags, int userId) { + ParceledListSlice packages; + try { + packages = mIPackageManager.getInstalledPackages(flags, userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + initBackgroundInstalledPackages(); + + ListIterator 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 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 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 getBackgroundInstalledPackages() { + return mBackgroundInstalledPackages; + } + + @VisibleForTesting + SparseArrayMap> 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 { + public final long startTimeStampMillis; + public long endTimeStampMillis; + public final Set 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 @@ public class BackgroundInstallControlService extends SystemService { 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 class BackgroundInstallControlService extends SystemService { 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 0000000000000..54fa272dd3b66 --- /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 mPackageListObserverCaptor; + @Captor + private ArgumentCaptor 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 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; + } + } +}