Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e4b8b94c authored by Chris Antol's avatar Chris Antol
Browse files

Introduce Settings Preference Service

Bug: 375193223
Test: atest CtsSettingsPreferenceServiceTest
Flag: com.android.settingslib.flags.settings_catalyst
Change-Id: I1b800dbf67923b694296690a4ed56a1fa9cc88a0
parent 9eb82f03
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
@@ -42149,6 +42149,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();
+8 −0
Original line number Diff line number Diff line
@@ -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 {
+18 −0
Original line number Diff line number Diff line
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;
}
+201 −0
Original line number Diff line number Diff line
/*
 * 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);
}
+248 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
Loading