Introduce Settings Preference Service

Bug: 375193223
Test: atest CtsSettingsPreferenceServiceTest
Flag: com.android.settingslib.flags.settings_catalyst
Change-Id: I1b800dbf67923b694296690a4ed56a1fa9cc88a0
diff --git a/core/api/current.txt b/core/api/current.txt
index 80928bf..ffecc4a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -42149,6 +42149,25 @@
     method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setWriteSensitivity(int);
   }
 
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public abstract class SettingsPreferenceService extends android.app.Service {
+    ctor public SettingsPreferenceService();
+    method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
+    method public abstract void onGetAllPreferenceMetadata(@NonNull android.service.settings.preferences.MetadataRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.MetadataResult,java.lang.Exception>);
+    method public abstract void onGetPreferenceValue(@NonNull android.service.settings.preferences.GetValueRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.GetValueResult,java.lang.Exception>);
+    method public abstract void onSetPreferenceValue(@NonNull android.service.settings.preferences.SetValueRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.SetValueResult,java.lang.Exception>);
+    field public static final String ACTION_PREFERENCE_SERVICE = "android.service.settings.preferences.action.PREFERENCE_SERVICE";
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public class SettingsPreferenceServiceClient implements java.lang.AutoCloseable {
+    ctor public SettingsPreferenceServiceClient(@NonNull android.content.Context, @NonNull String);
+    method public void close();
+    method public void getAllPreferenceMetadata(@NonNull android.service.settings.preferences.MetadataRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.MetadataResult,java.lang.Exception>);
+    method public void getPreferenceValue(@NonNull android.service.settings.preferences.GetValueRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.GetValueResult,java.lang.Exception>);
+    method public void setPreferenceValue(@NonNull android.service.settings.preferences.SetValueRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.SetValueResult,java.lang.Exception>);
+    method public void start();
+    method public void stop();
+  }
+
   @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SettingsPreferenceValue implements android.os.Parcelable {
     method public int describeContents();
     method public boolean getBooleanValue();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1173519..d4333fd 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3247,6 +3247,14 @@
 
 }
 
+package android.service.settings.preferences {
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public class SettingsPreferenceServiceClient implements java.lang.AutoCloseable {
+    ctor public SettingsPreferenceServiceClient(@NonNull android.content.Context, @NonNull String, boolean, @Nullable android.content.ServiceConnection);
+  }
+
+}
+
 package android.service.voice {
 
   public class AlwaysOnHotwordDetector implements android.service.voice.HotwordDetector {
diff --git a/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl b/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl
new file mode 100644
index 0000000..64a8b90
--- /dev/null
+++ b/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl
@@ -0,0 +1,18 @@
+package android.service.settings.preferences;
+
+import android.service.settings.preferences.GetValueRequest;
+import android.service.settings.preferences.IGetValueCallback;
+import android.service.settings.preferences.IMetadataCallback;
+import android.service.settings.preferences.ISetValueCallback;
+import android.service.settings.preferences.MetadataRequest;
+import android.service.settings.preferences.SetValueRequest;
+
+/** @hide */
+oneway interface ISettingsPreferenceService {
+    @EnforcePermission("READ_SYSTEM_PREFERENCES")
+    void getAllPreferenceMetadata(in MetadataRequest request, IMetadataCallback callback) = 1;
+    @EnforcePermission("READ_SYSTEM_PREFERENCES")
+    void getPreferenceValue(in GetValueRequest request, IGetValueCallback callback) = 2;
+    @EnforcePermission(allOf = {"READ_SYSTEM_PREFERENCES", "WRITE_SYSTEM_PREFERENCES"})
+    void setPreferenceValue(in SetValueRequest request, ISetValueCallback callback) = 3;
+}
diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceService.java b/core/java/android/service/settings/preferences/SettingsPreferenceService.java
new file mode 100644
index 0000000..4a4b5d2
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SettingsPreferenceService.java
@@ -0,0 +1,201 @@
+/*
+ * 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.service.settings.preferences;
+
+import android.Manifest;
+import android.annotation.EnforcePermission;
+import android.annotation.FlaggedApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.PermissionEnforcer;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.flags.Flags;
+
+/**
+ * Base class for a service that exposes its settings preferences to external access.
+ * <p>This class is to be implemented by apps that contribute to the Android Settings surface.
+ * Access to this service is permission guarded by
+ * {@link android.permission.READ_SYSTEM_PREFERENCES} for binding and reading, and guarded by both
+ * {@link android.permission.READ_SYSTEM_PREFERENCES} and
+ * {@link android.permission.WRITE_SYSTEM_PREFERENCES} for writing. An additional checks for access
+ * control are the responsibility of the implementing class.
+ *
+ * <p>This implementation must correspond to an exported service declaration in the host app
+ * AndroidManifest.xml as follows
+ * <pre class="prettyprint">
+ * {@literal
+ * <service
+ *     android:permission="android.permission.READ_SYSTEM_PREFERENCES"
+ *     android:exported="true">
+ *     <intent-filter>
+ *         <action android:name="android.service.settings.preferences.action.PREFERENCE_SERVICE" />
+ *     </intent-filter>
+ * </service>}
+ * </pre>
+ *
+ * <ul>
+ *   <li>It is recommended to expose the metadata for most, if not all, preferences within a
+ *   settings app, thus implementing {@link #onGetAllPreferenceMetadata}.
+ *   <li>Exposing preferences for read access of their values is up to the implementer, but any
+ *   exposed must be a subset of the preferences exposed in {@link #onGetAllPreferenceMetadata}.
+ *   To expose a preference for read access, the implementation will contain
+ *   {@link #onGetPreferenceValue}.
+ *   <li>Exposing a preference for write access of their values is up to the implementer, but should
+ *   be done so with extra care and consideration, both for security and privacy. These must also
+ *   be a subset of those exposed in {@link #onGetAllPreferenceMetadata}. To expose a preference for
+ *   write access, the implementation will contain {@link #onSetPreferenceValue}.
+ * </ul>
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public abstract class SettingsPreferenceService extends Service {
+
+    /**
+     * Intent Action corresponding to a {@link SettingsPreferenceService}. Note that any checks for
+     * such services must be accompanied by a check to ensure the host is a system application.
+     * Given an {@link android.content.pm.ApplicationInfo} you can check for
+     * {@link android.content.pm.ApplicationInfo#FLAG_SYSTEM}, or when querying
+     * {@link PackageManager#queryIntentServices} you can provide the flag
+     * {@link PackageManager#MATCH_SYSTEM_ONLY}.
+     */
+    public static final String ACTION_PREFERENCE_SERVICE =
+            "android.service.settings.preferences.action.PREFERENCE_SERVICE";
+
+    /** @hide */
+    @NonNull
+    @Override
+    public final IBinder onBind(@Nullable Intent intent) {
+        return new ISettingsPreferenceService.Stub(
+                PermissionEnforcer.fromContext(getApplicationContext())) {
+            @EnforcePermission(Manifest.permission.READ_SYSTEM_PREFERENCES)
+            @Override
+            public void getAllPreferenceMetadata(MetadataRequest request,
+                                                 IMetadataCallback callback) {
+                getAllPreferenceMetadata_enforcePermission();
+                onGetAllPreferenceMetadata(request, new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(MetadataResult result) {
+                        try {
+                            callback.onSuccess(result);
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception error) {
+                        try {
+                            callback.onFailure();
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+                });
+            }
+
+            @EnforcePermission(Manifest.permission.READ_SYSTEM_PREFERENCES)
+            @Override
+            public void getPreferenceValue(GetValueRequest request, IGetValueCallback callback) {
+                getPreferenceValue_enforcePermission();
+                onGetPreferenceValue(request, new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(GetValueResult result) {
+                        try {
+                            callback.onSuccess(result);
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception error) {
+                        try {
+                            callback.onFailure();
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+                });
+            }
+
+            @EnforcePermission(allOf = {
+                    Manifest.permission.READ_SYSTEM_PREFERENCES,
+                    Manifest.permission.WRITE_SYSTEM_PREFERENCES
+            })
+            @Override
+            public void setPreferenceValue(SetValueRequest request, ISetValueCallback callback) {
+                setPreferenceValue_enforcePermission();
+                onSetPreferenceValue(request, new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(SetValueResult result) {
+                        try {
+                            callback.onSuccess(result);
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception error) {
+                        try {
+                            callback.onFailure();
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+                });
+            }
+        };
+    }
+
+    /**
+     * Retrieve the metadata for all exposed settings preferences within this application. This
+     * data should be a snapshot of their state at the time of this method being called.
+     * @param request object to specify request parameters
+     * @param callback object to receive result or failure of request
+     */
+    public abstract void onGetAllPreferenceMetadata(
+            @NonNull MetadataRequest request,
+            @NonNull OutcomeReceiver<MetadataResult, Exception> callback);
+
+    /**
+     * Retrieve the current value of the requested settings preference. If this value is not exposed
+     * or cannot be obtained for some reason, the corresponding result code will be set on the
+     * result object.
+     * @param request object to specify request parameters
+     * @param callback object to receive result or failure of request
+     */
+    public abstract void onGetPreferenceValue(
+            @NonNull GetValueRequest request,
+            @NonNull OutcomeReceiver<GetValueResult, Exception> callback);
+
+    /**
+     * Set the value within the request to the target settings preference. If this value cannot
+     * be written for some reason, the corresponding result code will be set on the result object.
+     * @param request object to specify request parameters
+     * @param callback object to receive result or failure of request
+     */
+    public abstract void onSetPreferenceValue(
+            @NonNull SetValueRequest request,
+            @NonNull OutcomeReceiver<SetValueResult, Exception> callback);
+}
diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java b/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java
new file mode 100644
index 0000000..39995a4
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java
@@ -0,0 +1,248 @@
+/*
+ * 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.service.settings.preferences;
+
+import static android.service.settings.preferences.SettingsPreferenceService.ACTION_PREFERENCE_SERVICE;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.TestApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.flags.Flags;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Client class responsible for binding to and interacting with an instance of
+ * {@link SettingsPreferenceService}.
+ * <p>This is a convenience class to handle the lifecycle of the service connection.
+ * <p>This client will only interact with one instance at a time,
+ * so if the caller requires multiple instances (multiple applications that provide settings), then
+ * the caller must create multiple client classes, one for each instance required. To find all
+ * available services, a caller may query {@link android.content.pm.PackageManager} for applications
+ * that provide the intent action {@link SettingsPreferenceService#ACTION_PREFERENCE_SERVICE} that
+ * are also system applications ({@link android.content.pm.ApplicationInfo#FLAG_SYSTEM}).
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public class SettingsPreferenceServiceClient implements AutoCloseable {
+
+    private final Context mContext;
+    private final Intent mServiceIntent;
+    private final ServiceConnection mServiceConnection;
+    private final boolean mSystemOnly;
+    private ISettingsPreferenceService mRemoteService;
+
+    /**
+     * Construct a client for binding to a {@link SettingsPreferenceService} provided by the
+     * application corresponding to the provided package name.
+     * @param packageName - package name for which this client will initiate a service binding
+     */
+    public SettingsPreferenceServiceClient(@NonNull Context context,
+                                           @NonNull String packageName) {
+        this(context, packageName, true, null);
+    }
+
+    /**
+     * @hide Only to be called directly by test
+     */
+    @TestApi
+    public SettingsPreferenceServiceClient(@NonNull Context context,
+                                           @NonNull String packageName,
+                                           boolean systemOnly,
+                                           @Nullable ServiceConnection connectionListener) {
+        mContext = context.getApplicationContext();
+        mServiceIntent = new Intent(ACTION_PREFERENCE_SERVICE).setPackage(packageName);
+        mSystemOnly = systemOnly;
+        mServiceConnection = createServiceConnection(connectionListener);
+    }
+
+    /**
+     * Initiate binding to service.
+     * <p>If no service exists for the package provided or the package is not for a system
+     * application, no binding will occur.
+     */
+    public void start() {
+        PackageManager pm = mContext.getPackageManager();
+        PackageManager.ResolveInfoFlags flags;
+        if (mSystemOnly) {
+            flags = PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY);
+        } else {
+            flags = PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL);
+        }
+        List<ResolveInfo> infos = pm.queryIntentServices(mServiceIntent, flags);
+        if (infos.size() == 1) {
+            mContext.bindService(mServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
+        }
+    }
+
+    /**
+     * If there is an active service binding, unbind from that service.
+     */
+    public void stop() {
+        if (mRemoteService != null) {
+            mRemoteService = null;
+            mContext.unbindService(mServiceConnection);
+        }
+    }
+
+    /**
+     * Retrieve the metadata for all exposed settings preferences within the application.
+     * @param request object to specify request parameters
+     * @param executor {@link Executor} on which to invoke the receiver
+     * @param receiver callback to receive the result or failure
+     */
+    public void getAllPreferenceMetadata(
+            @NonNull MetadataRequest request,
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull OutcomeReceiver<MetadataResult, Exception> receiver) {
+        if (mRemoteService == null) {
+            executor.execute(() ->
+                    receiver.onError(new IllegalStateException("Service not ready")));
+            return;
+        }
+        try {
+            mRemoteService.getAllPreferenceMetadata(request, new IMetadataCallback.Stub() {
+                @Override
+                public void onSuccess(MetadataResult result) {
+                    executor.execute(() -> receiver.onResult(result));
+                }
+
+                @Override
+                public void onFailure() {
+                    executor.execute(() -> receiver.onError(
+                            new IllegalStateException("Service call failure")));
+                }
+            });
+        } catch (RemoteException | RuntimeException e) {
+            executor.execute(() -> receiver.onError(e));
+        }
+    }
+
+    /**
+     * Retrieve the current value of the requested settings preference.
+     * @param request object to specify request parameters
+     * @param executor {@link Executor} on which to invoke the receiver
+     * @param receiver callback to receive the result or failure
+     */
+    public void getPreferenceValue(@NonNull GetValueRequest request,
+                                   @CallbackExecutor @NonNull Executor executor,
+                                   @NonNull OutcomeReceiver<GetValueResult, Exception> receiver) {
+        if (mRemoteService == null) {
+            executor.execute(() ->
+                    receiver.onError(new IllegalStateException("Service not ready")));
+            return;
+        }
+        try {
+            mRemoteService.getPreferenceValue(request, new IGetValueCallback.Stub() {
+                @Override
+                public void onSuccess(GetValueResult result) {
+                    executor.execute(() -> receiver.onResult(result));
+                }
+
+                @Override
+                public void onFailure() {
+                    executor.execute(() -> receiver.onError(
+                            new IllegalStateException("Service call failure")));
+                }
+            });
+        } catch (RemoteException | RuntimeException e) {
+            executor.execute(() -> receiver.onError(e));
+        }
+    }
+
+    /**
+     * Set the value on the target settings preference.
+     * @param request object to specify request parameters
+     * @param executor {@link Executor} on which to invoke the receiver
+     * @param receiver callback to receive the result or failure
+     */
+    public void setPreferenceValue(@NonNull SetValueRequest request,
+                                   @CallbackExecutor @NonNull Executor executor,
+                                   @NonNull OutcomeReceiver<SetValueResult, Exception> receiver) {
+        if (mRemoteService == null) {
+            executor.execute(() ->
+                    receiver.onError(new IllegalStateException("Service not ready")));
+            return;
+        }
+        try {
+            mRemoteService.setPreferenceValue(request, new ISetValueCallback.Stub() {
+                @Override
+                public void onSuccess(SetValueResult result) {
+                    executor.execute(() -> receiver.onResult(result));
+                }
+
+                @Override
+                public void onFailure() {
+                    executor.execute(() -> receiver.onError(
+                            new IllegalStateException("Service call failure")));
+                }
+            });
+        } catch (RemoteException | RuntimeException e) {
+            executor.execute(() -> receiver.onError(e));
+        }
+    }
+
+    @NonNull
+    private ServiceConnection createServiceConnection(@Nullable ServiceConnection listener) {
+        return new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                mRemoteService = getPreferenceServiceInterface(service);
+                if (listener != null) {
+                    listener.onServiceConnected(name, service);
+                }
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                mRemoteService = null;
+                if (listener != null) {
+                    listener.onServiceDisconnected(name);
+                }
+            }
+        };
+    }
+
+    @NonNull
+    private ISettingsPreferenceService getPreferenceServiceInterface(@NonNull IBinder service) {
+        return ISettingsPreferenceService.Stub.asInterface(service);
+    }
+
+    /**
+     * This client handles a resource, thus is it important to appropriately close that resource
+     * when it is no longer needed.
+     * <p>This method is provided by {@link AutoCloseable} and calling it
+     * will unbind any service binding.
+     */
+    @Override
+    public void close() {
+        stop();
+    }
+}
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 7ced809..541ca60 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -594,6 +594,9 @@
         <!-- Permission required for CTS test - AdvancedProtectionManagerTest -->
         <permission name="android.permission.SET_ADVANCED_PROTECTION_MODE" />
         <permission name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" />
+        <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest -->
+        <permission name="android.permission.READ_SYSTEM_PREFERENCES" />
+        <permission name="android.permission.WRITE_SYSTEM_PREFERENCES" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 0724410..7b6321d 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -962,6 +962,10 @@
     <!-- Permission required for ExecutableMethodFileOffsetsTest -->
     <uses-permission android:name="android.permission.DYNAMIC_INSTRUMENTATION" />
 
+    <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest -->
+    <uses-permission android:name="android.permission.READ_SYSTEM_PREFERENCES" />
+    <uses-permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES" />
+
     <application
         android:label="@string/app_label"
         android:theme="@android:style/Theme.DeviceDefault.DayNight"