Merge "[AAPM] Add Feature class" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 0212fe3..0404b79 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -39834,7 +39834,7 @@
 
 package android.security.advancedprotection {
 
-  @FlaggedApi("android.security.aapm_api") public class AdvancedProtectionManager {
+  @FlaggedApi("android.security.aapm_api") public final class AdvancedProtectionManager {
     method @RequiresPermission(android.Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE) public boolean isAdvancedProtectionEnabled();
     method @RequiresPermission(android.Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE) public void registerAdvancedProtectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.security.advancedprotection.AdvancedProtectionManager.Callback);
     method @RequiresPermission(android.Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE) public void unregisterAdvancedProtectionCallback(@NonNull android.security.advancedprotection.AdvancedProtectionManager.Callback);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 9879e7f..967a938 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -12467,7 +12467,16 @@
 
 package android.security.advancedprotection {
 
-  @FlaggedApi("android.security.aapm_api") public class AdvancedProtectionManager {
+  @FlaggedApi("android.security.aapm_api") public final class AdvancedProtectionFeature implements android.os.Parcelable {
+    ctor public AdvancedProtectionFeature(@NonNull String);
+    method public int describeContents();
+    method @NonNull public String getId();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.security.advancedprotection.AdvancedProtectionFeature> CREATOR;
+  }
+
+  @FlaggedApi("android.security.aapm_api") public final class AdvancedProtectionManager {
+    method @NonNull @RequiresPermission(android.Manifest.permission.SET_ADVANCED_PROTECTION_MODE) public java.util.List<android.security.advancedprotection.AdvancedProtectionFeature> getAdvancedProtectionFeatures();
     method @RequiresPermission(android.Manifest.permission.SET_ADVANCED_PROTECTION_MODE) public void setAdvancedProtectionEnabled(boolean);
   }
 
diff --git a/core/java/android/security/advancedprotection/AdvancedProtectionFeature.aidl b/core/java/android/security/advancedprotection/AdvancedProtectionFeature.aidl
new file mode 100644
index 0000000..3ecef02
--- /dev/null
+++ b/core/java/android/security/advancedprotection/AdvancedProtectionFeature.aidl
@@ -0,0 +1,23 @@
+/*
+ * 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 android.security.advancedprotection;
+
+/**
+ * Represents an advanced protection feature providing protections
+ * @hide
+ */
+parcelable AdvancedProtectionFeature;
\ No newline at end of file
diff --git a/core/java/android/security/advancedprotection/AdvancedProtectionFeature.java b/core/java/android/security/advancedprotection/AdvancedProtectionFeature.java
new file mode 100644
index 0000000..a086bf7
--- /dev/null
+++ b/core/java/android/security/advancedprotection/AdvancedProtectionFeature.java
@@ -0,0 +1,77 @@
+/*
+ * 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 android.security.advancedprotection;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.security.Flags;
+
+/**
+ * An advanced protection feature providing protections.
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_AAPM_API)
+@SystemApi
+public final class AdvancedProtectionFeature implements Parcelable {
+    private final String mId;
+
+    /**
+     * Create an object identifying an Advanced Protection feature for AdvancedProtectionManager
+     * @param id A unique ID to identify this feature. It is used by Settings screens to display
+     *           information about this feature.
+     */
+    public AdvancedProtectionFeature(@NonNull String id) {
+        mId = id;
+    }
+
+    private AdvancedProtectionFeature(Parcel in) {
+        mId = in.readString8();
+    }
+
+    /**
+     * @return the unique ID representing this feature
+     */
+    @NonNull
+    public String getId() {
+        return mId;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(mId);
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<AdvancedProtectionFeature> CREATOR =
+            new Parcelable.Creator<>() {
+                public AdvancedProtectionFeature createFromParcel(Parcel in) {
+                    return new AdvancedProtectionFeature(in);
+                }
+
+                public AdvancedProtectionFeature[] newArray(int size) {
+                    return new AdvancedProtectionFeature[size];
+                }
+            };
+}
diff --git a/core/java/android/security/advancedprotection/AdvancedProtectionManager.java b/core/java/android/security/advancedprotection/AdvancedProtectionManager.java
index 43b6ebe..6f3e3d8 100644
--- a/core/java/android/security/advancedprotection/AdvancedProtectionManager.java
+++ b/core/java/android/security/advancedprotection/AdvancedProtectionManager.java
@@ -29,6 +29,7 @@
 import android.security.Flags;
 import android.util.Log;
 
+import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 
@@ -41,7 +42,7 @@
  */
 @FlaggedApi(Flags.FLAG_AAPM_API)
 @SystemService(Context.ADVANCED_PROTECTION_SERVICE)
-public class AdvancedProtectionManager {
+public final class AdvancedProtectionManager {
     private static final String TAG = "AdvancedProtectionMgr";
 
     private final ConcurrentHashMap<Callback, IAdvancedProtectionCallback>
@@ -147,6 +148,22 @@
     }
 
     /**
+     * Returns the list of advanced protection features which are available on this device.
+     *
+     * @hide
+     */
+    @SystemApi
+    @NonNull
+    @RequiresPermission(Manifest.permission.SET_ADVANCED_PROTECTION_MODE)
+    public List<AdvancedProtectionFeature> getAdvancedProtectionFeatures() {
+        try {
+            return mService.getAdvancedProtectionFeatures();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * A callback class for monitoring changes to Advanced Protection state
      *
      * <p>To register a callback, implement this interface, and register it with
diff --git a/core/java/android/security/advancedprotection/IAdvancedProtectionService.aidl b/core/java/android/security/advancedprotection/IAdvancedProtectionService.aidl
index ef0abf4..6830763 100644
--- a/core/java/android/security/advancedprotection/IAdvancedProtectionService.aidl
+++ b/core/java/android/security/advancedprotection/IAdvancedProtectionService.aidl
@@ -16,6 +16,7 @@
 
 package android.security.advancedprotection;
 
+import android.security.advancedprotection.AdvancedProtectionFeature;
 import android.security.advancedprotection.IAdvancedProtectionCallback;
 
 /**
@@ -32,4 +33,6 @@
     void unregisterAdvancedProtectionCallback(IAdvancedProtectionCallback callback);
     @EnforcePermission("SET_ADVANCED_PROTECTION_MODE")
     void setAdvancedProtectionEnabled(boolean enabled);
+    @EnforcePermission("SET_ADVANCED_PROTECTION_MODE")
+    List<AdvancedProtectionFeature> getAdvancedProtectionFeatures();
 }
\ No newline at end of file
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 c33ed55..1260eee 100644
--- a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java
+++ b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java
@@ -33,6 +33,7 @@
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
 import android.provider.Settings;
+import android.security.advancedprotection.AdvancedProtectionFeature;
 import android.security.advancedprotection.IAdvancedProtectionCallback;
 import android.security.advancedprotection.IAdvancedProtectionService;
 import android.util.ArrayMap;
@@ -44,9 +45,11 @@
 import com.android.server.SystemService;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.security.advancedprotection.features.AdvancedProtectionHook;
+import com.android.server.security.advancedprotection.features.AdvancedProtectionProvider;
 
 import java.io.FileDescriptor;
 import java.util.ArrayList;
+import java.util.List;
 
 /** @hide */
 public class AdvancedProtectionService extends IAdvancedProtectionService.Stub  {
@@ -58,10 +61,12 @@
     private final Handler mHandler;
     private final AdvancedProtectionStore mStore;
 
-    // Features owned by the service - their code will be executed when state changes
+    // Features living with 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<>();
+    // For tracking only - not called on state change
+    private final ArrayList<AdvancedProtectionProvider> mProviders = new ArrayList<>();
 
     private AdvancedProtectionService(@NonNull Context context) {
         super(PermissionEnforcer.fromContext(context));
@@ -71,13 +76,17 @@
     }
 
     private void initFeatures(boolean enabled) {
+        // Empty until features are added.
+        // Examples:
+        // mHooks.add(new SideloadingAdvancedProtectionHook(mContext, enabled));
+        // mProviders.add(new WifiAdvancedProtectionProvider());
     }
 
     // Only for tests
     @VisibleForTesting
     AdvancedProtectionService(@NonNull Context context, @NonNull AdvancedProtectionStore store,
             @NonNull Looper looper, @NonNull PermissionEnforcer permissionEnforcer,
-            @Nullable AdvancedProtectionHook hook) {
+            @Nullable AdvancedProtectionHook hook, @Nullable AdvancedProtectionProvider provider) {
         super(permissionEnforcer);
         mContext = context;
         mStore = store;
@@ -85,6 +94,10 @@
         if (hook != null) {
             mHooks.add(hook);
         }
+
+        if (provider != null) {
+            mProviders.add(provider);
+        }
     }
 
     @Override
@@ -146,6 +159,25 @@
     }
 
     @Override
+    @EnforcePermission(Manifest.permission.SET_ADVANCED_PROTECTION_MODE)
+    public List<AdvancedProtectionFeature> getAdvancedProtectionFeatures() {
+        getAdvancedProtectionFeatures_enforcePermission();
+        List<AdvancedProtectionFeature> features = new ArrayList<>();
+        for (int i = 0; i < mProviders.size(); i++) {
+            features.addAll(mProviders.get(i).getFeatures());
+        }
+
+        for (int i = 0; i < mHooks.size(); i++) {
+            AdvancedProtectionHook hook = mHooks.get(i);
+            if (hook.isAvailable()) {
+                features.add(hook.getFeature());
+            }
+        }
+
+        return features;
+    }
+
+    @Override
     public void onShellCommand(FileDescriptor in, FileDescriptor out,
             FileDescriptor err, @NonNull String[] args, ShellCallback callback,
             @NonNull ResultReceiver resultReceiver) {
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
index b2acc51..f82db96 100644
--- a/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionHook.java
+++ b/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionHook.java
@@ -18,11 +18,15 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.security.advancedprotection.AdvancedProtectionFeature;
 
 /** @hide */
 public abstract class AdvancedProtectionHook {
     /** Called on boot phase PHASE_SYSTEM_SERVICES_READY */
     public AdvancedProtectionHook(@NonNull Context context, boolean enabled) {}
+    /** The feature this hook provides */
+    @NonNull
+    public abstract AdvancedProtectionFeature getFeature();
     /** 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();
diff --git a/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionProvider.java b/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionProvider.java
new file mode 100644
index 0000000..ed451f1
--- /dev/null
+++ b/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionProvider.java
@@ -0,0 +1,27 @@
+/*
+ * 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.security.advancedprotection.AdvancedProtectionFeature;
+
+import java.util.List;
+
+/** @hide */
+public abstract class AdvancedProtectionProvider {
+    /** The list of features provided */
+    public abstract List<AdvancedProtectionFeature> getFeatures();
+}
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 e97b48c..24bf6ca 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
@@ -16,6 +16,8 @@
 
 package com.android.server.security.advancedprotection;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -28,15 +30,20 @@
 import android.os.test.FakePermissionEnforcer;
 import android.os.test.TestLooper;
 import android.provider.Settings;
+import android.security.advancedprotection.AdvancedProtectionFeature;
 import android.security.advancedprotection.IAdvancedProtectionCallback;
 
+import androidx.annotation.NonNull;
+
 import com.android.server.security.advancedprotection.features.AdvancedProtectionHook;
+import com.android.server.security.advancedprotection.features.AdvancedProtectionProvider;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 @SuppressLint("VisibleForTests")
@@ -47,6 +54,7 @@
     private Context mContext;
     private AdvancedProtectionService.AdvancedProtectionStore mStore;
     private TestLooper mLooper;
+    AdvancedProtectionFeature mFeature = new AdvancedProtectionFeature("test-id");
 
     @Before
     public void setup() throws Settings.SettingNotFoundException {
@@ -70,8 +78,9 @@
         };
 
         mLooper = new TestLooper();
+
         mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
-                mPermissionEnforcer, null);
+                mPermissionEnforcer, null, null);
     }
 
     @Test
@@ -93,6 +102,12 @@
         AtomicBoolean callbackCaptor = new AtomicBoolean(false);
         AdvancedProtectionHook hook =
                 new AdvancedProtectionHook(mContext, true) {
+                    @NonNull
+                    @Override
+                    public AdvancedProtectionFeature getFeature() {
+                        return mFeature;
+                    }
+
                     @Override
                     public boolean isAvailable() {
                         return true;
@@ -105,7 +120,7 @@
                 };
 
         mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
-                mPermissionEnforcer, hook);
+                mPermissionEnforcer, hook, null);
         mService.setAdvancedProtectionEnabled(true);
         mLooper.dispatchNext();
 
@@ -117,6 +132,12 @@
         AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false);
         AdvancedProtectionHook hook =
                 new AdvancedProtectionHook(mContext, true) {
+                    @NonNull
+                    @Override
+                    public AdvancedProtectionFeature getFeature() {
+                        return mFeature;
+                    }
+
                     @Override
                     public boolean isAvailable() {
                         return false;
@@ -129,7 +150,8 @@
                 };
 
         mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
-                mPermissionEnforcer, hook);
+                mPermissionEnforcer, hook, null);
+
         mService.setAdvancedProtectionEnabled(true);
         mLooper.dispatchNext();
         assertFalse(callbackCalledCaptor.get());
@@ -140,6 +162,12 @@
         AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false);
         AdvancedProtectionHook hook =
                 new AdvancedProtectionHook(mContext, true) {
+                    @NonNull
+                    @Override
+                    public AdvancedProtectionFeature getFeature() {
+                        return mFeature;
+                    }
+
                     @Override
                     public boolean isAvailable() {
                         return true;
@@ -152,7 +180,7 @@
                 };
 
         mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
-                mPermissionEnforcer, hook);
+                mPermissionEnforcer, hook, null);
         mService.setAdvancedProtectionEnabled(true);
         mLooper.dispatchNext();
         assertTrue(callbackCalledCaptor.get());
@@ -208,6 +236,66 @@
         assertFalse(callbackCalledCaptor.get());
     }
 
+    @Test
+    public void testGetFeatures() {
+        AdvancedProtectionFeature feature1 = new AdvancedProtectionFeature("id-1");
+        AdvancedProtectionFeature feature2 = new AdvancedProtectionFeature("id-2");
+        AdvancedProtectionHook hook = new AdvancedProtectionHook(mContext, true) {
+            @NonNull
+            @Override
+            public AdvancedProtectionFeature getFeature() {
+                return feature1;
+            }
+
+            @Override
+            public boolean isAvailable() {
+                return true;
+            }
+        };
+
+        AdvancedProtectionProvider provider = new AdvancedProtectionProvider() {
+            @Override
+            public List<AdvancedProtectionFeature> getFeatures() {
+                return List.of(feature2);
+            }
+        };
+
+        mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
+                mPermissionEnforcer, hook, provider);
+        List<AdvancedProtectionFeature> features = mService.getAdvancedProtectionFeatures();
+        assertThat(features, containsInAnyOrder(feature1, feature2));
+    }
+
+    @Test
+    public void testGetFeatures_featureNotAvailable() {
+        AdvancedProtectionFeature feature1 = new AdvancedProtectionFeature("id-1");
+        AdvancedProtectionFeature feature2 = new AdvancedProtectionFeature("id-2");
+        AdvancedProtectionHook hook = new AdvancedProtectionHook(mContext, true) {
+            @NonNull
+            @Override
+            public AdvancedProtectionFeature getFeature() {
+                return feature1;
+            }
+
+            @Override
+            public boolean isAvailable() {
+                return false;
+            }
+        };
+
+        AdvancedProtectionProvider provider = new AdvancedProtectionProvider() {
+            @Override
+            public List<AdvancedProtectionFeature> getFeatures() {
+                return List.of(feature2);
+            }
+        };
+
+        mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(),
+                mPermissionEnforcer, hook, provider);
+        List<AdvancedProtectionFeature> features = mService.getAdvancedProtectionFeatures();
+        assertThat(features, containsInAnyOrder(feature2));
+    }
+
 
     @Test
     public void testSetProtection_withoutPermission() {