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

Commit fa67e3a6 authored by Chris Antol's avatar Chris Antol Committed by Android (Google) Code Review
Browse files

Merge "Introduce Settings Preference Service" into main

parents 5299e1a8 e4b8b94c
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
@@ -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();
+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