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"