[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()));
+    }
 }