[AAPM] Add abstract class for feature hooks
We add a class that platform features can extend, to provide custom aapm
behavior. Callbacks in the class are called whenever AAPM state changes.
Bug: 352420507
Test: atest AdvancedProtectionServiceTest AdvancedProtectionManagerTest
Flag: android.security.aapm_api
Change-Id: Icdf930de1c88fe209f00b0fe5f04184ec84bb8a8
diff --git a/core/java/android/security/advancedprotection/AdvancedProtectionManager.java b/core/java/android/security/advancedprotection/AdvancedProtectionManager.java
index 59dd680f..43b6ebe 100644
--- a/core/java/android/security/advancedprotection/AdvancedProtectionManager.java
+++ b/core/java/android/security/advancedprotection/AdvancedProtectionManager.java
@@ -42,7 +42,7 @@
@FlaggedApi(Flags.FLAG_AAPM_API)
@SystemService(Context.ADVANCED_PROTECTION_SERVICE)
public class AdvancedProtectionManager {
- private static final String TAG = "AdvancedProtectionM";
+ private static final String TAG = "AdvancedProtectionMgr";
private final ConcurrentHashMap<Callback, IAdvancedProtectionCallback>
mCallbackMap = new ConcurrentHashMap<>();
@@ -73,7 +73,7 @@
* Registers a {@link Callback} to be notified of changes to the Advanced Protection state.
*
* <p>The provided callback will be called on the specified executor with the updated
- * {@link AdvancedProtectionState}. Methods are called when the state changes, as well as once
+ * state. Methods are called when the state changes, as well as once
* on initial registration.
*
* @param executor The executor of where the callback will execute.
diff --git a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java
index 9a63c823..c33ed55 100644
--- a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java
+++ b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java
@@ -21,6 +21,7 @@
import android.Manifest;
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.os.Binder;
import android.os.Handler;
@@ -35,37 +36,55 @@
import android.security.advancedprotection.IAdvancedProtectionCallback;
import android.security.advancedprotection.IAdvancedProtectionService;
import android.util.ArrayMap;
+import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.pm.UserManagerInternal;
+import com.android.server.security.advancedprotection.features.AdvancedProtectionHook;
import java.io.FileDescriptor;
import java.util.ArrayList;
/** @hide */
public class AdvancedProtectionService extends IAdvancedProtectionService.Stub {
+ private static final String TAG = "AdvancedProtectionService";
private static final int MODE_CHANGED = 0;
private static final int CALLBACK_ADDED = 1;
+ private final Context mContext;
private final Handler mHandler;
private final AdvancedProtectionStore mStore;
+
+ // Features owned by the service - their code will be executed when state changes
+ private final ArrayList<AdvancedProtectionHook> mHooks = new ArrayList<>();
+ // External features - they will be called on state change
private final ArrayMap<IBinder, IAdvancedProtectionCallback> mCallbacks = new ArrayMap<>();
private AdvancedProtectionService(@NonNull Context context) {
super(PermissionEnforcer.fromContext(context));
+ mContext = context;
mHandler = new AdvancedProtectionHandler(FgThread.get().getLooper());
mStore = new AdvancedProtectionStore(context);
}
+ private void initFeatures(boolean enabled) {
+ }
+
+ // Only for tests
@VisibleForTesting
AdvancedProtectionService(@NonNull Context context, @NonNull AdvancedProtectionStore store,
- @NonNull Looper looper, @NonNull PermissionEnforcer permissionEnforcer) {
+ @NonNull Looper looper, @NonNull PermissionEnforcer permissionEnforcer,
+ @Nullable AdvancedProtectionHook hook) {
super(permissionEnforcer);
+ mContext = context;
mStore = store;
mHandler = new AdvancedProtectionHandler(looper);
+ if (hook != null) {
+ mHooks.add(hook);
+ }
}
@Override
@@ -100,8 +119,8 @@
@Override
@EnforcePermission(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE)
- public void unregisterAdvancedProtectionCallback(@NonNull IAdvancedProtectionCallback callback)
- throws RemoteException {
+ public void unregisterAdvancedProtectionCallback(
+ @NonNull IAdvancedProtectionCallback callback) {
unregisterAdvancedProtectionCallback_enforcePermission();
synchronized (mCallbacks) {
mCallbacks.remove(callback.asBinder());
@@ -118,6 +137,7 @@
if (enabled != isAdvancedProtectionEnabledInternal()) {
mStore.store(enabled);
sendModeChanged(enabled);
+ Slog.i(TAG, "Advanced protection is " + (enabled ? "enabled" : "disabled"));
}
}
} finally {
@@ -156,6 +176,17 @@
public void onStart() {
publishBinderService(Context.ADVANCED_PROTECTION_SERVICE, mService);
}
+
+ @Override
+ public void onBootPhase(@BootPhase int phase) {
+ if (phase == PHASE_SYSTEM_SERVICES_READY) {
+ boolean enabled = mService.isAdvancedProtectionEnabledInternal();
+ if (enabled) {
+ Slog.i(TAG, "Advanced protection is enabled");
+ }
+ mService.initFeatures(enabled);
+ }
+ }
}
@VisibleForTesting
@@ -205,6 +236,12 @@
private void handleAllCallbacks(boolean enabled) {
ArrayList<IAdvancedProtectionCallback> deadObjects = new ArrayList<>();
+ for (int i = 0; i < mHooks.size(); i++) {
+ AdvancedProtectionHook feature = mHooks.get(i);
+ if (feature.isAvailable()) {
+ feature.onAdvancedProtectionChanged(enabled);
+ }
+ }
synchronized (mCallbacks) {
for (int i = 0; i < mCallbacks.size(); i++) {
IAdvancedProtectionCallback callback = mCallbacks.valueAt(i);
diff --git a/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionHook.java b/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionHook.java
new file mode 100644
index 0000000..b2acc51
--- /dev/null
+++ b/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionHook.java
@@ -0,0 +1,31 @@
+/*
+ * 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.security.advancedprotection.features;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+/** @hide */
+public abstract class AdvancedProtectionHook {
+ /** Called on boot phase PHASE_SYSTEM_SERVICES_READY */
+ public AdvancedProtectionHook(@NonNull Context context, boolean enabled) {}
+ /** Whether this feature is relevant on this device. If false, onAdvancedProtectionChanged will
+ * not be called, and the feature will not be displayed in the onboarding UX. */
+ public abstract boolean isAvailable();
+ /** Called whenever advanced protection state changes */
+ public void onAdvancedProtectionChanged(boolean enabled) {}
+}
diff --git a/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java b/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java
index 727b435..e97b48c 100644
--- a/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java
@@ -28,58 +28,188 @@
import android.os.test.FakePermissionEnforcer;
import android.os.test.TestLooper;
import android.provider.Settings;
+import android.security.advancedprotection.IAdvancedProtectionCallback;
+
+import com.android.server.security.advancedprotection.features.AdvancedProtectionHook;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@SuppressLint("VisibleForTests")
@RunWith(JUnit4.class)
public class AdvancedProtectionServiceTest {
private AdvancedProtectionService mService;
private FakePermissionEnforcer mPermissionEnforcer;
private Context mContext;
+ private AdvancedProtectionService.AdvancedProtectionStore mStore;
+ private TestLooper mLooper;
@Before
- @SuppressLint("VisibleForTests")
public void setup() throws Settings.SettingNotFoundException {
mContext = mock(Context.class);
mPermissionEnforcer = new FakePermissionEnforcer();
mPermissionEnforcer.grant(Manifest.permission.SET_ADVANCED_PROTECTION_MODE);
mPermissionEnforcer.grant(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE);
- AdvancedProtectionService.AdvancedProtectionStore store =
- new AdvancedProtectionService.AdvancedProtectionStore(mContext) {
- private boolean mEnabled = false;
+ mStore = new AdvancedProtectionService.AdvancedProtectionStore(mContext) {
+ private boolean mEnabled = false;
- @Override
- boolean retrieve() {
- return mEnabled;
- }
+ @Override
+ boolean retrieve() {
+ return mEnabled;
+ }
- @Override
- void store(boolean enabled) {
- this.mEnabled = enabled;
- }
- };
+ @Override
+ void store(boolean enabled) {
+ this.mEnabled = enabled;
+ }
+ };
- mService = new AdvancedProtectionService(mContext, store, new TestLooper().getLooper(),
- mPermissionEnforcer);
+ mLooper = new TestLooper();
+ mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
+ mPermissionEnforcer, null);
}
@Test
- public void testEnableProtection() throws RemoteException {
+ public void testToggleProtection() {
mService.setAdvancedProtectionEnabled(true);
assertTrue(mService.isAdvancedProtectionEnabled());
- }
- @Test
- public void testDisableProtection() throws RemoteException {
mService.setAdvancedProtectionEnabled(false);
assertFalse(mService.isAdvancedProtectionEnabled());
}
@Test
+ public void testDisableProtection_byDefault() {
+ assertFalse(mService.isAdvancedProtectionEnabled());
+ }
+
+ @Test
+ public void testEnableProtection_withHook() {
+ AtomicBoolean callbackCaptor = new AtomicBoolean(false);
+ AdvancedProtectionHook hook =
+ new AdvancedProtectionHook(mContext, true) {
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public void onAdvancedProtectionChanged(boolean enabled) {
+ callbackCaptor.set(enabled);
+ }
+ };
+
+ mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
+ mPermissionEnforcer, hook);
+ mService.setAdvancedProtectionEnabled(true);
+ mLooper.dispatchNext();
+
+ assertTrue(callbackCaptor.get());
+ }
+
+ @Test
+ public void testEnableProtection_withFeature_notAvailable() {
+ AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false);
+ AdvancedProtectionHook hook =
+ new AdvancedProtectionHook(mContext, true) {
+ @Override
+ public boolean isAvailable() {
+ return false;
+ }
+
+ @Override
+ public void onAdvancedProtectionChanged(boolean enabled) {
+ callbackCalledCaptor.set(true);
+ }
+ };
+
+ mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
+ mPermissionEnforcer, hook);
+ mService.setAdvancedProtectionEnabled(true);
+ mLooper.dispatchNext();
+ assertFalse(callbackCalledCaptor.get());
+ }
+
+ @Test
+ public void testEnableProtection_withFeature_notCalledIfModeNotChanged() {
+ AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false);
+ AdvancedProtectionHook hook =
+ new AdvancedProtectionHook(mContext, true) {
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public void onAdvancedProtectionChanged(boolean enabled) {
+ callbackCalledCaptor.set(true);
+ }
+ };
+
+ mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
+ mPermissionEnforcer, hook);
+ mService.setAdvancedProtectionEnabled(true);
+ mLooper.dispatchNext();
+ assertTrue(callbackCalledCaptor.get());
+
+ callbackCalledCaptor.set(false);
+ mService.setAdvancedProtectionEnabled(true);
+ mLooper.dispatchAll();
+ assertFalse(callbackCalledCaptor.get());
+ }
+
+ @Test
+ public void testRegisterCallback() throws RemoteException {
+ AtomicBoolean callbackCaptor = new AtomicBoolean(false);
+ IAdvancedProtectionCallback callback = new IAdvancedProtectionCallback.Stub() {
+ @Override
+ public void onAdvancedProtectionChanged(boolean enabled) {
+ callbackCaptor.set(enabled);
+ }
+ };
+
+ mService.setAdvancedProtectionEnabled(true);
+ mLooper.dispatchAll();
+
+ mService.registerAdvancedProtectionCallback(callback);
+ mLooper.dispatchNext();
+ assertTrue(callbackCaptor.get());
+
+ mService.setAdvancedProtectionEnabled(false);
+ mLooper.dispatchNext();
+
+ assertFalse(callbackCaptor.get());
+ }
+
+ @Test
+ public void testUnregisterCallback() throws RemoteException {
+ AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false);
+ IAdvancedProtectionCallback callback = new IAdvancedProtectionCallback.Stub() {
+ @Override
+ public void onAdvancedProtectionChanged(boolean enabled) {
+ callbackCalledCaptor.set(true);
+ }
+ };
+
+ mService.setAdvancedProtectionEnabled(true);
+ mService.registerAdvancedProtectionCallback(callback);
+ mLooper.dispatchAll();
+ callbackCalledCaptor.set(false);
+
+ mService.unregisterAdvancedProtectionCallback(callback);
+ mService.setAdvancedProtectionEnabled(false);
+
+ mLooper.dispatchNext();
+ assertFalse(callbackCalledCaptor.get());
+ }
+
+
+ @Test
public void testSetProtection_withoutPermission() {
mPermissionEnforcer.revoke(Manifest.permission.SET_ADVANCED_PROTECTION_MODE);
assertThrows(SecurityException.class, () -> mService.setAdvancedProtectionEnabled(true));
@@ -90,4 +220,18 @@
mPermissionEnforcer.revoke(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE);
assertThrows(SecurityException.class, () -> mService.isAdvancedProtectionEnabled());
}
+
+ @Test
+ public void testRegisterCallback_withoutPermission() {
+ mPermissionEnforcer.revoke(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE);
+ assertThrows(SecurityException.class, () -> mService.registerAdvancedProtectionCallback(
+ new IAdvancedProtectionCallback.Default()));
+ }
+
+ @Test
+ public void testUnregisterCallback_withoutPermission() {
+ mPermissionEnforcer.revoke(Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE);
+ assertThrows(SecurityException.class, () -> mService.unregisterAdvancedProtectionCallback(
+ new IAdvancedProtectionCallback.Default()));
+ }
}