diff options
10 files changed, 424 insertions, 5 deletions
diff --git a/core/java/android/content/pm/IBackgroundInstallControlService.aidl b/core/java/android/content/pm/IBackgroundInstallControlService.aidl index c8e7caebc821..2e7f19ed2e95 100644 --- a/core/java/android/content/pm/IBackgroundInstallControlService.aidl +++ b/core/java/android/content/pm/IBackgroundInstallControlService.aidl @@ -17,10 +17,15 @@ package android.content.pm; import android.content.pm.ParceledListSlice; +import android.os.IRemoteCallback; /** * {@hide} */ interface IBackgroundInstallControlService { ParceledListSlice getBackgroundInstalledPackages(long flags, int userId); + + void registerBackgroundInstallCallback(IRemoteCallback callback); + + void unregisterBackgroundInstallCallback(IRemoteCallback callback); } diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java new file mode 100644 index 000000000000..155361837071 --- /dev/null +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 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 android.os.Process.THREAD_PRIORITY_BACKGROUND; + +import android.annotation.NonNull; +import android.app.BackgroundInstallControlManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IRemoteCallback; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.ServiceThread; + +public class BackgroundInstallControlCallbackHelper { + + @VisibleForTesting static final String FLAGGED_PACKAGE_NAME_KEY = "packageName"; + @VisibleForTesting static final String FLAGGED_USER_ID_KEY = "userId"; + private static final String TAG = "BackgroundInstallControlCallbackHelper"; + + private final Handler mHandler; + + BackgroundInstallControlCallbackHelper() { + HandlerThread backgroundThread = + new ServiceThread( + "BackgroundInstallControlCallbackHelperBg", + THREAD_PRIORITY_BACKGROUND, + true); + backgroundThread.start(); + mHandler = new Handler(backgroundThread.getLooper()); + } + + @NonNull @VisibleForTesting + final RemoteCallbackList<IRemoteCallback> mCallbacks = new RemoteCallbackList<>(); + + /** Registers callback that gets invoked upon detection of an MBA + * + * NOTE: The callback is user context agnostic and currently broadcasts to all users of other + * users app installs. This is fine because the API is for SystemServer use only. + */ + public void registerBackgroundInstallCallback(IRemoteCallback callback) { + synchronized (mCallbacks) { + mCallbacks.register(callback, null); + } + } + + /** Unregisters callback */ + public void unregisterBackgroundInstallCallback(IRemoteCallback callback) { + synchronized (mCallbacks) { + mCallbacks.unregister(callback); + } + } + + /** + * Invokes all registered callbacks Callbacks are processed through user provided-threads and + * parameters are passed in via {@link BackgroundInstallControlManager} InstallEvent + */ + public void notifyAllCallbacks(int userId, String packageName) { + Bundle extras = new Bundle(); + extras.putCharSequence(FLAGGED_PACKAGE_NAME_KEY, packageName); + extras.putInt(FLAGGED_USER_ID_KEY, userId); + synchronized (mCallbacks) { + mHandler.post( + () -> + mCallbacks.broadcast( + callback -> { + try { + callback.sendResult(extras); + } catch (RemoteException e) { + Slog.e( + TAG, + "error detected: " + e.getLocalizedMessage(), + e); + } + })); + } + } +} diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index 200b17bc2f97..3468081088a3 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -36,6 +36,7 @@ import android.os.Binder; import android.os.Build; import android.os.Environment; import android.os.Handler; +import android.os.IRemoteCallback; import android.os.Looper; import android.os.Message; import android.os.SystemClock; @@ -93,6 +94,8 @@ public class BackgroundInstallControlService extends SystemService { private final File mDiskFile; private final Context mContext; + private final BackgroundInstallControlCallbackHelper mCallbackHelper; + private SparseSetArray<String> mBackgroundInstalledPackages = null; // User ID -> package name -> set of foreground time frame @@ -112,6 +115,7 @@ public class BackgroundInstallControlService extends SystemService { mHandler = new EventHandler(injector.getLooper(), this); mDiskFile = injector.getDiskFile(); mContext = injector.getContext(); + mCallbackHelper = injector.getBackgroundInstallControlCallbackHelper(); UsageStatsManagerInternal usageStatsManagerInternal = injector.getUsageStatsManagerInternal(); usageStatsManagerInternal.registerListener( @@ -150,6 +154,16 @@ public class BackgroundInstallControlService extends SystemService { } } + @Override + public void registerBackgroundInstallCallback(IRemoteCallback callback) { + mService.mCallbackHelper.registerBackgroundInstallCallback(callback); + } + + @Override + public void unregisterBackgroundInstallCallback(IRemoteCallback callback) { + mService.mCallbackHelper.unregisterBackgroundInstallCallback(callback); + } + } @RequiresPermission(GET_BACKGROUND_INSTALLED_PACKAGES) @@ -274,6 +288,7 @@ public class BackgroundInstallControlService extends SystemService { initBackgroundInstalledPackages(); mBackgroundInstalledPackages.add(userId, packageName); + mCallbackHelper.notifyAllCallbacks(userId, packageName); writeBackgroundInstalledPackagesToDisk(); } @@ -568,6 +583,8 @@ public class BackgroundInstallControlService extends SystemService { File getDiskFile(); + BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper(); + } private static final class InjectorImpl implements Injector { @@ -617,5 +634,10 @@ public class BackgroundInstallControlService extends SystemService { File file = new File(dir, DISK_FILE_NAME); return file; } + + @Override + public BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper() { + return new BackgroundInstallControlCallbackHelper(); + } } } diff --git a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp index d479e52f92d8..682ed91c22dd 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp +++ b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp @@ -32,6 +32,7 @@ java_test_host { ":BackgroundInstallControlServiceTestApp", ":BackgroundInstallControlMockApp1", ":BackgroundInstallControlMockApp2", + ":BackgroundInstallControlMockApp3", ], test_suites: [ "general-tests", diff --git a/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml b/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml index 1e7a78aa6f93..a352851d1297 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml +++ b/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml @@ -34,6 +34,9 @@ <option name="push-file" key="BackgroundInstallControlMockApp2.apk" value="/data/local/tmp/BackgroundInstallControlMockApp2.apk" /> + <option name="push-file" + key="BackgroundInstallControlMockApp3.apk" + value="/data/local/tmp/BackgroundInstallControlMockApp3.apk" /> </target_preparer> <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > diff --git a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java index c99e7129853b..5092a4659eb9 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java +++ b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java @@ -68,6 +68,20 @@ public final class BackgroundInstallControlServiceHostTest extends BaseHostJUnit assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_2)).isNull(); } + @Test + public void testRegisterCallback() throws Exception { + runDeviceTest( + "BackgroundInstallControlServiceTest", + "testRegisterBackgroundInstallControlCallback"); + } + + @Test + public void testUnregisterCallback() throws Exception { + runDeviceTest( + "BackgroundInstallControlServiceTest", + "testUnregisterBackgroundInstallControlCallback"); + } + private void installPackage(String path) throws DeviceNotAvailableException { String cmd = "pm install -t --force-queryable " + path; CommandResult result = getDevice().executeShellV2Command(cmd); diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java index b23f59106881..ac041f492135 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java +++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java @@ -16,38 +16,59 @@ package com.android.server.pm.test.app; -import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES; - import static com.android.compatibility.common.util.SystemUtil.runShellCommand; import static com.google.common.truth.Truth.assertThat; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.IBackgroundInstallControlService; import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; +import android.os.Bundle; +import android.os.IRemoteCallback; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; +import android.util.Pair; +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ShellIdentityUtils; +import com.android.compatibility.common.util.ThrowingRunnable; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.FileInputStream; +import java.io.OutputStream; +import java.util.ArrayList; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.stream.Collectors; @RunWith(AndroidJUnit4.class) public class BackgroundInstallControlServiceTest { private static final String TAG = "BackgroundInstallControlServiceTest"; + private static final String ACTION_INSTALL_COMMIT = + "com.android.server.pm.test.app.BackgroundInstallControlServiceTest" + + ".ACTION_INSTALL_COMMIT"; private static final String MOCK_PACKAGE_NAME = "com.android.servicestests.apps.bicmockapp3"; + private static final String TEST_DATA_DIR = "/data/local/tmp/"; + + private static final String MOCK_APK_FILE = "BackgroundInstallControlMockApp3.apk"; private IBackgroundInstallControlService mIBics; @Before @@ -74,10 +95,9 @@ public class BackgroundInstallControlServiceTest { PackageManager.MATCH_ALL, Process.myUserHandle() .getIdentifier()); } catch (RemoteException e) { - throw new RuntimeException(e); + throw e.rethrowFromSystemServer(); } - }, - GET_BACKGROUND_INSTALLED_PACKAGES); + }); assertThat(slice).isNotNull(); var packageList = slice.getList(); @@ -94,4 +114,150 @@ public class BackgroundInstallControlServiceTest { .collect(Collectors.toSet()); assertThat(actualPackageNames).containsExactlyElementsIn(expectedPackageNames); } + + @Test + public void testRegisterBackgroundInstallControlCallback() + throws Exception { + String testPackageName = "test"; + int testUserId = 1; + ArrayList<Pair<String, Integer>> sharedResource = new ArrayList<>(); + IRemoteCallback testCallback = + new IRemoteCallback.Stub() { + private final ArrayList<Pair<String, Integer>> mArray = sharedResource; + + @Override + public void sendResult(Bundle data) throws RemoteException { + mArray.add(new Pair(testPackageName, testUserId)); + } + }; + ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn( + mIBics, + (bics) -> { + try { + bics.registerBackgroundInstallCallback(testCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + }); + installPackage(TEST_DATA_DIR + MOCK_APK_FILE, MOCK_PACKAGE_NAME); + + assertUntil(() -> sharedResource.size() == 1, 2000); + assertThat(sharedResource.get(0).first).isEqualTo(testPackageName); + assertThat(sharedResource.get(0).second).isEqualTo(testUserId); + } + + @Test + public void testUnregisterBackgroundInstallControlCallback() { + String testValue = "test"; + ArrayList<String> sharedResource = new ArrayList<>(); + IRemoteCallback testCallback = + new IRemoteCallback.Stub() { + private final ArrayList<String> mArray = sharedResource; + + @Override + public void sendResult(Bundle data) throws RemoteException { + mArray.add(testValue); + } + }; + ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn( + mIBics, + (bics) -> { + try { + bics.registerBackgroundInstallCallback(testCallback); + bics.unregisterBackgroundInstallCallback(testCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + }); + installPackage(TEST_DATA_DIR + MOCK_APK_FILE, MOCK_PACKAGE_NAME); + + assertUntil(sharedResource::isEmpty, 2000); + } + + private static boolean installPackage(String apkPath, String packageName) { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + final CountDownLatch installLatch = new CountDownLatch(1); + final BroadcastReceiver installReceiver = + new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + int packageInstallStatus = + intent.getIntExtra( + PackageInstaller.EXTRA_STATUS, + PackageInstaller.STATUS_FAILURE_INVALID); + if (packageInstallStatus == PackageInstaller.STATUS_SUCCESS) { + installLatch.countDown(); + } + } + }; + final IntentFilter intentFilter = new IntentFilter(ACTION_INSTALL_COMMIT); + context.registerReceiver(installReceiver, intentFilter, Context.RECEIVER_EXPORTED); + + PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); + PackageInstaller.SessionParams params = + new PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL); + params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED); + try { + int sessionId = packageInstaller.createSession(params); + PackageInstaller.Session session = packageInstaller.openSession(sessionId); + OutputStream out = session.openWrite(packageName, 0, -1); + FileInputStream fis = new FileInputStream(apkPath); + byte[] buffer = new byte[65536]; + int size; + while ((size = fis.read(buffer)) != -1) { + out.write(buffer, 0, size); + } + session.fsync(out); + fis.close(); + out.close(); + + runWithShellPermissionIdentity( + () -> { + session.commit(createPendingIntent(context).getIntentSender()); + installLatch.await(5, TimeUnit.SECONDS); + }); + return true; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static PendingIntent createPendingIntent(Context context) { + PendingIntent pendingIntent = + PendingIntent.getBroadcast( + context, + 1, + new Intent(ACTION_INSTALL_COMMIT) + .setPackage( + BackgroundInstallControlServiceTest.class.getPackageName()), + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE); + return pendingIntent; + } + + private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable command) + throws Exception { + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + try { + command.run(); + } finally { + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + } + + private static void assertUntil(Supplier<Boolean> condition, int timeoutMs) { + long endTime = System.currentTimeMillis() + timeoutMs; + while (System.currentTimeMillis() <= endTime) { + if (condition.get()) return; + try { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + assertThat(condition.get()).isTrue(); + } }
\ No newline at end of file diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp index 7804f4ce9d02..39b0ff782b72 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp +++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp @@ -50,3 +50,11 @@ android_test_helper_app { "--rename-manifest-package com.android.servicestests.apps.bicmockapp2", ], } + +android_test_helper_app { + name: "BackgroundInstallControlMockApp3", + defaults: ["bic-mock-app-defaults"], + aaptflags: [ + "--rename-manifest-package com.android.servicestests.apps.bicmockapp3", + ], +} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java new file mode 100644 index 000000000000..574f3699edb8 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 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 com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_PACKAGE_NAME_KEY; +import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_USER_ID_KEY; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.os.Bundle; +import android.os.IRemoteCallback; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +/** Unit tests for {@link BackgroundInstallControlCallbackHelper} */ +@Presubmit +@RunWith(JUnit4.class) +public class BackgroundInstallControlCallbackHelperTest { + + private final IRemoteCallback mCallback = + spy( + new IRemoteCallback.Stub() { + @Override + public void sendResult(Bundle extras) {} + }); + + private BackgroundInstallControlCallbackHelper mCallbackHelper; + + @Before + public void setup() { + mCallbackHelper = new BackgroundInstallControlCallbackHelper(); + } + + @Test + public void registerBackgroundInstallControlCallback_registers_successfully() { + mCallbackHelper.registerBackgroundInstallCallback(mCallback); + + synchronized (mCallbackHelper.mCallbacks) { + assertEquals(1, mCallbackHelper.mCallbacks.getRegisteredCallbackCount()); + assertEquals(mCallback, mCallbackHelper.mCallbacks.getRegisteredCallbackItem(0)); + } + } + + @Test + public void unregisterBackgroundInstallControlCallback_unregisters_successfully() { + synchronized (mCallbackHelper.mCallbacks) { + mCallbackHelper.mCallbacks.register(mCallback); + } + + mCallbackHelper.unregisterBackgroundInstallCallback(mCallback); + + synchronized (mCallbackHelper.mCallbacks) { + assertEquals(0, mCallbackHelper.mCallbacks.getRegisteredCallbackCount()); + } + } + + @Test + public void notifyAllCallbacks_broadcastsToCallbacks() + throws RemoteException { + String testPackageName = "testname"; + int testUserId = 1; + mCallbackHelper.registerBackgroundInstallCallback(mCallback); + + mCallbackHelper.notifyAllCallbacks(testUserId, testPackageName); + + ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); + verify(mCallback, after(1000).times(1)).sendResult(bundleCaptor.capture()); + Bundle receivedBundle = bundleCaptor.getValue(); + assertEquals(testPackageName, receivedBundle.getString(FLAGGED_PACKAGE_NAME_KEY)); + assertEquals(testUserId, receivedBundle.getInt(FLAGGED_USER_ID_KEY)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java index 8656f60afc1e..bf87e3ac1f7e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java @@ -111,6 +111,8 @@ public final class BackgroundInstallControlServiceTest { private UsageStatsManagerInternal mUsageStatsManagerInternal; @Mock private PermissionManagerServiceInternal mPermissionManager; + @Mock + private BackgroundInstallControlCallbackHelper mCallbackHelper; @Captor private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor; @@ -982,5 +984,11 @@ public final class BackgroundInstallControlServiceTest { public File getDiskFile() { return mFile; } + + + @Override + public BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper() { + return mCallbackHelper; + } } } |