diff options
| author | 2024-11-08 23:42:36 +0000 | |
|---|---|---|
| committer | 2024-11-08 23:42:36 +0000 | |
| commit | fa67e3a694b095b30a9387d56f68684703358d5e (patch) | |
| tree | ce92c84f572dd3198538511d0c2cdc0cd470191b | |
| parent | 5299e1a8aa3da0182720691fc55d42ee3e92b94b (diff) | |
| parent | e4b8b94c7f9fd0ae61fb7f09555ca860c0fcfdef (diff) | |
Merge "Introduce Settings Preference Service" into main
7 files changed, 501 insertions, 0 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index c0b6ab6a6755..dbb4ce3bf81d 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -42159,6 +42159,25 @@ package android.service.settings.preferences { 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 8dd12172fdc5..3ca55b932e19 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3247,6 +3247,14 @@ package android.service.quicksettings { } +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 000000000000..64a8b90fe581 --- /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 000000000000..4a4b5d201f09 --- /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 000000000000..39995a47fcbe --- /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 7ced809d2a3a..541ca602a386 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -594,6 +594,9 @@ applications that come with the platform <!-- 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 0724410a2954..7b6321d1cc7d 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" |