Automatic sources dropoff on 2020-06-10 18:32:38.095721
The change is generated with prebuilt drop tool.
Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/android/service/appprediction/AppPredictionService.java b/android/service/appprediction/AppPredictionService.java
new file mode 100644
index 0000000..be20570
--- /dev/null
+++ b/android/service/appprediction/AppPredictionService.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2018 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.appprediction;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionSessionId;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.app.prediction.IPredictionCallback;
+import android.content.Intent;
+import android.content.pm.ParceledListSlice;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.service.appprediction.IPredictionService.Stub;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A service used to predict app and shortcut usage.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public abstract class AppPredictionService extends Service {
+
+ private static final String TAG = "AppPredictionService";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ *
+ * <p>The service must also require the {@link android.permission#MANAGE_APP_PREDICTIONS}
+ * permission.
+ *
+ * @hide
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.appprediction.AppPredictionService";
+
+ private final ArrayMap<AppPredictionSessionId, ArrayList<CallbackWrapper>> mSessionCallbacks =
+ new ArrayMap<>();
+ private Handler mHandler;
+
+ private final IPredictionService mInterface = new Stub() {
+
+ @Override
+ public void onCreatePredictionSession(AppPredictionContext context,
+ AppPredictionSessionId sessionId) {
+ mHandler.sendMessage(
+ obtainMessage(AppPredictionService::doCreatePredictionSession,
+ AppPredictionService.this, context, sessionId));
+ }
+
+ @Override
+ public void notifyAppTargetEvent(AppPredictionSessionId sessionId, AppTargetEvent event) {
+ mHandler.sendMessage(
+ obtainMessage(AppPredictionService::onAppTargetEvent,
+ AppPredictionService.this, sessionId, event));
+ }
+
+ @Override
+ public void notifyLaunchLocationShown(AppPredictionSessionId sessionId,
+ String launchLocation, ParceledListSlice targetIds) {
+ mHandler.sendMessage(
+ obtainMessage(AppPredictionService::onLaunchLocationShown,
+ AppPredictionService.this, sessionId, launchLocation,
+ targetIds.getList()));
+ }
+
+ @Override
+ public void sortAppTargets(AppPredictionSessionId sessionId, ParceledListSlice targets,
+ IPredictionCallback callback) {
+ mHandler.sendMessage(
+ obtainMessage(AppPredictionService::onSortAppTargets,
+ AppPredictionService.this, sessionId, targets.getList(), null,
+ new CallbackWrapper(callback, null)));
+ }
+
+ @Override
+ public void registerPredictionUpdates(AppPredictionSessionId sessionId,
+ IPredictionCallback callback) {
+ mHandler.sendMessage(
+ obtainMessage(AppPredictionService::doRegisterPredictionUpdates,
+ AppPredictionService.this, sessionId, callback));
+ }
+
+ @Override
+ public void unregisterPredictionUpdates(AppPredictionSessionId sessionId,
+ IPredictionCallback callback) {
+ mHandler.sendMessage(
+ obtainMessage(AppPredictionService::doUnregisterPredictionUpdates,
+ AppPredictionService.this, sessionId, callback));
+ }
+
+ @Override
+ public void requestPredictionUpdate(AppPredictionSessionId sessionId) {
+ mHandler.sendMessage(
+ obtainMessage(AppPredictionService::doRequestPredictionUpdate,
+ AppPredictionService.this, sessionId));
+ }
+
+ @Override
+ public void onDestroyPredictionSession(AppPredictionSessionId sessionId) {
+ mHandler.sendMessage(
+ obtainMessage(AppPredictionService::doDestroyPredictionSession,
+ AppPredictionService.this, sessionId));
+ }
+ };
+
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler(Looper.getMainLooper(), null, true);
+ }
+
+ @Override
+ @NonNull
+ public final IBinder onBind(@NonNull Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mInterface.asBinder();
+ }
+ Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+ return null;
+ }
+
+ /**
+ * Called by a client app to indicate a target launch
+ */
+ @MainThread
+ public abstract void onAppTargetEvent(@NonNull AppPredictionSessionId sessionId,
+ @NonNull AppTargetEvent event);
+
+ /**
+ * Called by a client app to indication a particular location has been shown to the user.
+ */
+ @MainThread
+ public abstract void onLaunchLocationShown(@NonNull AppPredictionSessionId sessionId,
+ @NonNull String launchLocation, @NonNull List<AppTargetId> targetIds);
+
+ private void doCreatePredictionSession(@NonNull AppPredictionContext context,
+ @NonNull AppPredictionSessionId sessionId) {
+ mSessionCallbacks.put(sessionId, new ArrayList<>());
+ onCreatePredictionSession(context, sessionId);
+ }
+
+ /**
+ * Creates a new interaction session.
+ *
+ * @param context interaction context
+ * @param sessionId the session's Id
+ */
+ public void onCreatePredictionSession(@NonNull AppPredictionContext context,
+ @NonNull AppPredictionSessionId sessionId) {}
+
+ /**
+ * Called by the client app to request sorting of targets based on prediction rank.
+ */
+ @MainThread
+ public abstract void onSortAppTargets(@NonNull AppPredictionSessionId sessionId,
+ @NonNull List<AppTarget> targets, @NonNull CancellationSignal cancellationSignal,
+ @NonNull Consumer<List<AppTarget>> callback);
+
+ private void doRegisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId,
+ @NonNull IPredictionCallback callback) {
+ final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
+ if (callbacks == null) {
+ Slog.e(TAG, "Failed to register for updates for unknown session: " + sessionId);
+ return;
+ }
+
+ final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback);
+ if (wrapper == null) {
+ callbacks.add(new CallbackWrapper(callback,
+ callbackWrapper ->
+ mHandler.post(() -> removeCallbackWrapper(callbacks, callbackWrapper))));
+ if (callbacks.size() == 1) {
+ onStartPredictionUpdates();
+ }
+ }
+ }
+
+ /**
+ * Called when any continuous prediction callback is registered.
+ */
+ @MainThread
+ public void onStartPredictionUpdates() {}
+
+ private void doUnregisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId,
+ @NonNull IPredictionCallback callback) {
+ final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
+ if (callbacks == null) {
+ Slog.e(TAG, "Failed to unregister for updates for unknown session: " + sessionId);
+ return;
+ }
+
+ final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback);
+ if (wrapper != null) {
+ removeCallbackWrapper(callbacks, wrapper);
+ }
+ }
+
+ private void removeCallbackWrapper(
+ ArrayList<CallbackWrapper> callbacks, CallbackWrapper wrapper) {
+ if (callbacks == null) {
+ return;
+ }
+ callbacks.remove(wrapper);
+ if (callbacks.isEmpty()) {
+ onStopPredictionUpdates();
+ }
+ }
+
+ /**
+ * Called when there are no longer any continuous prediction callbacks registered.
+ */
+ @MainThread
+ public void onStopPredictionUpdates() {}
+
+ private void doRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId) {
+ // Just an optimization, if there are no callbacks, then don't bother notifying the service
+ final ArrayList<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
+ if (callbacks != null && !callbacks.isEmpty()) {
+ onRequestPredictionUpdate(sessionId);
+ }
+ }
+
+ /**
+ * Called by the client app to request target predictions. This method is only called if there
+ * are one or more prediction callbacks registered.
+ *
+ * @see #updatePredictions(AppPredictionSessionId, List)
+ */
+ @MainThread
+ public abstract void onRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId);
+
+ private void doDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {
+ mSessionCallbacks.remove(sessionId);
+ onDestroyPredictionSession(sessionId);
+ }
+
+ /**
+ * Destroys the interaction session.
+ *
+ * @param sessionId the id of the session to destroy
+ */
+ @MainThread
+ public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {}
+
+ /**
+ * Used by the prediction factory to send back results the client app. The can be called
+ * in response to {@link #onRequestPredictionUpdate(AppPredictionSessionId)} or proactively as
+ * a result of changes in predictions.
+ */
+ public final void updatePredictions(@NonNull AppPredictionSessionId sessionId,
+ @NonNull List<AppTarget> targets) {
+ List<CallbackWrapper> callbacks = mSessionCallbacks.get(sessionId);
+ if (callbacks != null) {
+ for (CallbackWrapper callback : callbacks) {
+ callback.accept(targets);
+ }
+ }
+ }
+
+ /**
+ * Finds the callback wrapper for the given callback.
+ */
+ private CallbackWrapper findCallbackWrapper(ArrayList<CallbackWrapper> callbacks,
+ IPredictionCallback callback) {
+ for (int i = callbacks.size() - 1; i >= 0; i--) {
+ if (callbacks.get(i).isCallback(callback)) {
+ return callbacks.get(i);
+ }
+ }
+ return null;
+ }
+
+ private static final class CallbackWrapper implements Consumer<List<AppTarget>>,
+ IBinder.DeathRecipient {
+
+ private IPredictionCallback mCallback;
+ private final Consumer<CallbackWrapper> mOnBinderDied;
+
+ CallbackWrapper(IPredictionCallback callback,
+ @Nullable Consumer<CallbackWrapper> onBinderDied) {
+ mCallback = callback;
+ mOnBinderDied = onBinderDied;
+ try {
+ mCallback.asBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to link to death: " + e);
+ }
+ }
+
+ public boolean isCallback(@NonNull IPredictionCallback callback) {
+ if (mCallback == null) {
+ Slog.e(TAG, "Callback is null, likely the binder has died.");
+ return false;
+ }
+ return mCallback.equals(callback);
+ }
+
+ @Override
+ public void accept(List<AppTarget> ts) {
+ try {
+ if (mCallback != null) {
+ mCallback.onResult(new ParceledListSlice(ts));
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error sending result:" + e);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ mCallback = null;
+ if (mOnBinderDied != null) {
+ mOnBinderDied.accept(this);
+ }
+ }
+ }
+}
diff --git a/android/service/attention/AttentionService.java b/android/service/attention/AttentionService.java
new file mode 100644
index 0000000..49ab5db
--- /dev/null
+++ b/android/service/attention/AttentionService.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2019 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.attention;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
+/**
+ * Abstract base class for Attention service.
+ *
+ * <p> An attention service provides attention estimation related features to the system.
+ * The system's default AttentionService implementation is configured in
+ * {@code config_AttentionComponent}. If this config has no value, a stub is returned.
+ *
+ * See: {@link com.android.server.attention.AttentionManagerService}.
+ *
+ * <pre>
+ * {@literal
+ * <service android:name=".YourAttentionService"
+ * android:permission="android.permission.BIND_ATTENTION_SERVICE">
+ * </service>}
+ * </pre>
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class AttentionService extends Service {
+ /**
+ * The {@link Intent} that must be declared as handled by the service. To be supported, the
+ * service must also require the {@link android.Manifest.permission#BIND_ATTENTION_SERVICE}
+ * permission so that other applications can not abuse it.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.attention.AttentionService";
+
+ /** Attention is absent. */
+ public static final int ATTENTION_SUCCESS_ABSENT = 0;
+
+ /** Attention is present. */
+ public static final int ATTENTION_SUCCESS_PRESENT = 1;
+
+ /** Unknown reasons for failing to determine the attention. */
+ public static final int ATTENTION_FAILURE_UNKNOWN = 2;
+
+ /** Request has been cancelled. */
+ public static final int ATTENTION_FAILURE_CANCELLED = 3;
+
+ /** Preempted by other client. */
+ public static final int ATTENTION_FAILURE_PREEMPTED = 4;
+
+ /** Request timed out. */
+ public static final int ATTENTION_FAILURE_TIMED_OUT = 5;
+
+ /** Camera permission is not granted. */
+ public static final int ATTENTION_FAILURE_CAMERA_PERMISSION_ABSENT = 6;
+
+ /**
+ * Result codes for when attention check was successful.
+ *
+ * @hide
+ */
+ @IntDef(prefix = {"ATTENTION_SUCCESS_"}, value = {ATTENTION_SUCCESS_ABSENT,
+ ATTENTION_SUCCESS_PRESENT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AttentionSuccessCodes {
+ }
+
+ /**
+ * Result codes explaining why attention check was not successful.
+ *
+ * @hide
+ */
+ @IntDef(prefix = {"ATTENTION_FAILURE_"}, value = {ATTENTION_FAILURE_UNKNOWN,
+ ATTENTION_FAILURE_CANCELLED, ATTENTION_FAILURE_PREEMPTED, ATTENTION_FAILURE_TIMED_OUT,
+ ATTENTION_FAILURE_CAMERA_PERMISSION_ABSENT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AttentionFailureCodes {
+ }
+
+ private final IAttentionService.Stub mBinder = new IAttentionService.Stub() {
+
+ /** {@inheritDoc} */
+ @Override
+ public void checkAttention(IAttentionCallback callback) {
+ Preconditions.checkNotNull(callback);
+ AttentionService.this.onCheckAttention(new AttentionCallback(callback));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void cancelAttentionCheck(IAttentionCallback callback) {
+ Preconditions.checkNotNull(callback);
+ AttentionService.this.onCancelAttentionCheck(new AttentionCallback(callback));
+ }
+ };
+
+ @Nullable
+ @Override
+ public final IBinder onBind(@NonNull Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mBinder;
+ }
+ return null;
+ }
+
+ /**
+ * Checks the user attention and calls into the provided callback.
+ *
+ * @param callback the callback to return the result to
+ */
+ public abstract void onCheckAttention(@NonNull AttentionCallback callback);
+
+ /**
+ * Cancels pending work for a given callback.
+ *
+ * Implementation must call back with a failure code of {@link #ATTENTION_FAILURE_CANCELLED}.
+ */
+ public abstract void onCancelAttentionCheck(@NonNull AttentionCallback callback);
+
+ /** Callbacks for AttentionService results. */
+ public static final class AttentionCallback {
+ @NonNull private final IAttentionCallback mCallback;
+
+ private AttentionCallback(@NonNull IAttentionCallback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Signals a success and provides the result code.
+ *
+ * @param timestamp of when the attention signal was computed; system throttles the requests
+ * so this is useful to know how fresh the result is.
+ */
+ public void onSuccess(@AttentionSuccessCodes int result, long timestamp) {
+ try {
+ mCallback.onSuccess(result, timestamp);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Signals a failure and provides the error code. */
+ public void onFailure(@AttentionFailureCodes int error) {
+ try {
+ mCallback.onFailure(error);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+}
diff --git a/android/service/autofill/AutofillFieldClassificationService.java b/android/service/autofill/AutofillFieldClassificationService.java
new file mode 100644
index 0000000..28842a7
--- /dev/null
+++ b/android/service/autofill/AutofillFieldClassificationService.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2018 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.autofill;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A service that calculates field classification scores.
+ *
+ * <p>A field classification score is a {@code float} representing how well an
+ * {@link AutofillValue} filled matches a expected value predicted by an autofill service
+ * —a full match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
+ *
+ * <p>The exact score depends on the algorithm used to calculate it—the service must provide
+ * at least one default algorithm (which is used when the algorithm is not specified or is invalid),
+ * but it could provide more (in which case the algorithm name should be specified by the caller
+ * when calculating the scores).
+ *
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public abstract class AutofillFieldClassificationService extends Service {
+
+ private static final String TAG = "AutofillFieldClassificationService";
+
+ /**
+ * The {@link Intent} action that must be declared as handled by a service
+ * in its manifest for the system to recognize it as a quota providing service.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.autofill.AutofillFieldClassificationService";
+
+ /**
+ * Manifest metadata key for the resource string containing the name of the default field
+ * classification algorithm.
+ */
+ public static final String SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM =
+ "android.autofill.field_classification.default_algorithm";
+ /**
+ * Manifest metadata key for the resource string array containing the names of all field
+ * classification algorithms provided by the service.
+ */
+ public static final String SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS =
+ "android.autofill.field_classification.available_algorithms";
+
+ /**
+ * Field classification algorithm that computes the edit distance between two Strings.
+ *
+ * <p>Service implementation must provide this algorithm.</p>
+ */
+ public static final String REQUIRED_ALGORITHM_EDIT_DISTANCE = "EDIT_DISTANCE";
+
+ /**
+ * Field classification algorithm that computes whether the last four digits between two
+ * Strings match exactly.
+ *
+ * <p>Service implementation must provide this algorithm.</p>
+ */
+ public static final String REQUIRED_ALGORITHM_EXACT_MATCH = "EXACT_MATCH";
+
+ /**
+ * Field classification algorithm that compares a credit card string to known last four digits.
+ *
+ * <p>Service implementation must provide this algorithm.</p>
+ */
+ public static final String REQUIRED_ALGORITHM_CREDIT_CARD = "CREDIT_CARD";
+
+ /** {@hide} **/
+ public static final String EXTRA_SCORES = "scores";
+
+ private AutofillFieldClassificationServiceWrapper mWrapper;
+
+ private void calculateScores(RemoteCallback callback, List<AutofillValue> actualValues,
+ String[] userDataValues, String[] categoryIds, String defaultAlgorithm,
+ Bundle defaultArgs, Map algorithms, Map args) {
+ final Bundle data = new Bundle();
+ final float[][] scores = onCalculateScores(actualValues, Arrays.asList(userDataValues),
+ Arrays.asList(categoryIds), defaultAlgorithm, defaultArgs, algorithms, args);
+ if (scores != null) {
+ data.putParcelable(EXTRA_SCORES, new Scores(scores));
+ }
+ callback.sendResult(data);
+ }
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
+
+ /** @hide */
+ @SystemApi
+ @TestApi
+ public AutofillFieldClassificationService() {
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mWrapper = new AutofillFieldClassificationServiceWrapper();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mWrapper;
+ }
+
+ /**
+ * Calculates field classification scores in a batch.
+ *
+ * <p>A field classification score is a {@code float} representing how well an
+ * {@link AutofillValue} filled matches a expected value predicted by an autofill service
+ * —a full match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
+ *
+ * <p>The exact score depends on the algorithm used to calculate it—the service must
+ * provide at least one default algorithm (which is used when the algorithm is not specified
+ * or is invalid), but it could provide more (in which case the algorithm name should be
+ * specified by the caller when calculating the scores).
+ *
+ * <p>For example, if the service provides an algorithm named {@code EXACT_MATCH} that
+ * returns {@code 1.0} if all characters match or {@code 0.0} otherwise, a call to:
+ *
+ * <pre>
+ * service.onGetScores("EXACT_MATCH", null,
+ * Arrays.asList(AutofillValue.forText("email1"), AutofillValue.forText("PHONE1")),
+ * Arrays.asList("email1", "phone1"));
+ * </pre>
+ *
+ * <p>Returns:
+ *
+ * <pre>
+ * [
+ * [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
+ * [0.0, 0.0] // "PHONE1" compared against ["email1", "phone1"]
+ * ];
+ * </pre>
+ *
+ * <p>If the same algorithm allows the caller to specify whether the comparisons should be
+ * case sensitive by passing a boolean option named {@code "case_sensitive"}, then a call to:
+ *
+ * <pre>
+ * Bundle algorithmOptions = new Bundle();
+ * algorithmOptions.putBoolean("case_sensitive", false);
+ *
+ * service.onGetScores("EXACT_MATCH", algorithmOptions,
+ * Arrays.asList(AutofillValue.forText("email1"), AutofillValue.forText("PHONE1")),
+ * Arrays.asList("email1", "phone1"));
+ * </pre>
+ *
+ * <p>Returns:
+ *
+ * <pre>
+ * [
+ * [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
+ * [0.0, 1.0] // "PHONE1" compared against ["email1", "phone1"]
+ * ];
+ * </pre>
+ *
+ * @param algorithm name of the algorithm to be used to calculate the scores. If invalid or
+ * {@code null}, the default algorithm is used instead.
+ * @param algorithmOptions optional arguments to be passed to the algorithm.
+ * @param actualValues values entered by the user.
+ * @param userDataValues values predicted from the user data.
+ * @return the calculated scores of {@code actualValues} x {@code userDataValues}.
+ *
+ * {@hide}
+ *
+ * @deprecated Use {@link AutofillFieldClassificationService#onCalculateScores} instead.
+ */
+ @Nullable
+ @SystemApi
+ @Deprecated
+ public float[][] onGetScores(@Nullable String algorithm,
+ @Nullable Bundle algorithmOptions, @NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues) {
+ Log.e(TAG, "service implementation (" + getClass() + " does not implement onGetScores()");
+ return null;
+ }
+
+ /**
+ * Calculates field classification scores in a batch.
+ *
+ * <p>A field classification score is a {@code float} representing how well an
+ * {@link AutofillValue} matches a expected value predicted by an autofill service
+ * —a full match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
+ *
+ * <p>The exact score depends on the algorithm used to calculate it—the service must
+ * provide at least one default algorithm (which is used when the algorithm is not specified
+ * or is invalid), but it could provide more (in which case the algorithm name should be
+ * specified by the caller when calculating the scores).
+ *
+ * <p>For example, if the service provides an algorithm named {@code EXACT_MATCH} that
+ * returns {@code 1.0} if all characters match or {@code 0.0} otherwise, a call to:
+ *
+ * <pre>
+ * HashMap algorithms = new HashMap<>();
+ * algorithms.put("email", "EXACT_MATCH");
+ * algorithms.put("phone", "EXACT_MATCH");
+ *
+ * HashMap args = new HashMap<>();
+ * args.put("email", null);
+ * args.put("phone", null);
+ *
+ * service.onCalculateScores(Arrays.asList(AutofillValue.forText("email1"),
+ * AutofillValue.forText("PHONE1")), Arrays.asList("email1", "phone1"),
+ * Array.asList("email", "phone"), algorithms, args);
+ * </pre>
+ *
+ * <p>Returns:
+ *
+ * <pre>
+ * [
+ * [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
+ * [0.0, 0.0] // "PHONE1" compared against ["email1", "phone1"]
+ * ];
+ * </pre>
+ *
+ * <p>If the same algorithm allows the caller to specify whether the comparisons should be
+ * case sensitive by passing a boolean option named {@code "case_sensitive"}, then a call to:
+ *
+ * <pre>
+ * Bundle algorithmOptions = new Bundle();
+ * algorithmOptions.putBoolean("case_sensitive", false);
+ * args.put("phone", algorithmOptions);
+ *
+ * service.onCalculateScores(Arrays.asList(AutofillValue.forText("email1"),
+ * AutofillValue.forText("PHONE1")), Arrays.asList("email1", "phone1"),
+ * Array.asList("email", "phone"), algorithms, args);
+ * </pre>
+ *
+ * <p>Returns:
+ *
+ * <pre>
+ * [
+ * [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
+ * [0.0, 1.0] // "PHONE1" compared against ["email1", "phone1"]
+ * ];
+ * </pre>
+ *
+ * @param actualValues values entered by the user.
+ * @param userDataValues values predicted from the user data.
+ * @param categoryIds category Ids correspoinding to userDataValues
+ * @param defaultAlgorithm default field classification algorithm
+ * @param algorithms array of field classification algorithms
+ * @return the calculated scores of {@code actualValues} x {@code userDataValues}.
+ *
+ * {@hide}
+ */
+ @Nullable
+ @SystemApi
+ public float[][] onCalculateScores(@NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues, @NonNull List<String> categoryIds,
+ @Nullable String defaultAlgorithm, @Nullable Bundle defaultArgs,
+ @Nullable Map algorithms, @Nullable Map args) {
+ Log.e(TAG, "service implementation (" + getClass()
+ + " does not implement onCalculateScore()");
+ return null;
+ }
+
+ private final class AutofillFieldClassificationServiceWrapper
+ extends IAutofillFieldClassificationService.Stub {
+ @Override
+ public void calculateScores(RemoteCallback callback, List<AutofillValue> actualValues,
+ String[] userDataValues, String[] categoryIds, String defaultAlgorithm,
+ Bundle defaultArgs, Map algorithms, Map args)
+ throws RemoteException {
+ mHandler.sendMessage(obtainMessage(
+ AutofillFieldClassificationService::calculateScores,
+ AutofillFieldClassificationService.this,
+ callback, actualValues, userDataValues, categoryIds, defaultAlgorithm,
+ defaultArgs, algorithms, args));
+ }
+ }
+
+ /**
+ * Helper class used to encapsulate a float[][] in a Parcelable.
+ *
+ * {@hide}
+ */
+ public static final class Scores implements Parcelable {
+ @NonNull
+ public final float[][] scores;
+
+ private Scores(Parcel parcel) {
+ final int size1 = parcel.readInt();
+ final int size2 = parcel.readInt();
+ scores = new float[size1][size2];
+ for (int i = 0; i < size1; i++) {
+ for (int j = 0; j < size2; j++) {
+ scores[i][j] = parcel.readFloat();
+ }
+ }
+ }
+
+ private Scores(@NonNull float[][] scores) {
+ this.scores = scores;
+ }
+
+ @Override
+ public String toString() {
+ final int size1 = scores.length;
+ final int size2 = size1 > 0 ? scores[0].length : 0;
+ final StringBuilder builder = new StringBuilder("Scores [")
+ .append(size1).append("x").append(size2).append("] ");
+ for (int i = 0; i < size1; i++) {
+ builder.append(i).append(": ").append(Arrays.toString(scores[i])).append(' ');
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ int size1 = scores.length;
+ int size2 = scores[0].length;
+ parcel.writeInt(size1);
+ parcel.writeInt(size2);
+ for (int i = 0; i < size1; i++) {
+ for (int j = 0; j < size2; j++) {
+ parcel.writeFloat(scores[i][j]);
+ }
+ }
+ }
+
+ public static final @android.annotation.NonNull Creator<Scores> CREATOR = new Creator<Scores>() {
+ @Override
+ public Scores createFromParcel(Parcel parcel) {
+ return new Scores(parcel);
+ }
+
+ @Override
+ public Scores[] newArray(int size) {
+ return new Scores[size];
+ }
+ };
+ }
+}
diff --git a/android/service/autofill/AutofillService.java b/android/service/autofill/AutofillService.java
new file mode 100644
index 0000000..188670d
--- /dev/null
+++ b/android/service/autofill/AutofillService.java
@@ -0,0 +1,714 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.app.Service;
+import android.content.Intent;
+import android.os.BaseBundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+/**
+ * An {@code AutofillService} is a service used to automatically fill the contents of the screen
+ * on behalf of a given user - for more information about autofill, read
+ * <a href="{@docRoot}preview/features/autofill.html">Autofill Framework</a>.
+ *
+ * <p>An {@code AutofillService} is only bound to the Android System for autofill purposes if:
+ * <ol>
+ * <li>It requires the {@code android.permission.BIND_AUTOFILL_SERVICE} permission in its
+ * manifest.
+ * <li>The user explicitly enables it using Android Settings (the
+ * {@link Settings#ACTION_REQUEST_SET_AUTOFILL_SERVICE} intent can be used to launch such
+ * Settings screen).
+ * </ol>
+ *
+ * <a name="BasicUsage"></a>
+ * <h3>Basic usage</h3>
+ *
+ * <p>The basic autofill process is defined by the workflow below:
+ * <ol>
+ * <li>User focus an editable {@link View}.
+ * <li>View calls {@link AutofillManager#notifyViewEntered(android.view.View)}.
+ * <li>A {@link ViewStructure} representing all views in the screen is created.
+ * <li>The Android System binds to the service and calls {@link #onConnected()}.
+ * <li>The service receives the view structure through the
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)}.
+ * <li>The service replies through {@link FillCallback#onSuccess(FillResponse)}.
+ * <li>The Android System calls {@link #onDisconnected()} and unbinds from the
+ * {@code AutofillService}.
+ * <li>The Android System displays an autofill UI with the options sent by the service.
+ * <li>The user picks an option.
+ * <li>The proper views are autofilled.
+ * </ol>
+ *
+ * <p>This workflow was designed to minimize the time the Android System is bound to the service;
+ * for each call, it: binds to service, waits for the reply, and unbinds right away. Furthermore,
+ * those calls are considered stateless: if the service needs to keep state between calls, it must
+ * do its own state management (keeping in mind that the service's process might be killed by the
+ * Android System when unbound; for example, if the device is running low in memory).
+ *
+ * <p>Typically, the
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} will:
+ * <ol>
+ * <li>Parse the view structure looking for autofillable views (for example, using
+ * {@link android.app.assist.AssistStructure.ViewNode#getAutofillHints()}.
+ * <li>Match the autofillable views with the user's data.
+ * <li>Create a {@link Dataset} for each set of user's data that match those fields.
+ * <li>Fill the dataset(s) with the proper {@link AutofillId}s and {@link AutofillValue}s.
+ * <li>Add the dataset(s) to the {@link FillResponse} passed to
+ * {@link FillCallback#onSuccess(FillResponse)}.
+ * </ol>
+ *
+ * <p>For example, for a login screen with username and password views where the user only has one
+ * account in the service, the response could be:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder()
+ * .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer"))
+ * .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer"))
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>But if the user had 2 accounts instead, the response could be:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder()
+ * .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer"))
+ * .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer"))
+ * .build())
+ * .addDataset(new Dataset.Builder()
+ * .setValue(id1, AutofillValue.forText("flanders"), createPresentation("flanders"))
+ * .setValue(id2, AutofillValue.forText("OkelyDokelyDo"), createPresentation("password for flanders"))
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>If the service does not find any autofillable view in the view structure, it should pass
+ * {@code null} to {@link FillCallback#onSuccess(FillResponse)}; if the service encountered an error
+ * processing the request, it should call {@link FillCallback#onFailure(CharSequence)}. For
+ * performance reasons, it's paramount that the service calls either
+ * {@link FillCallback#onSuccess(FillResponse)} or {@link FillCallback#onFailure(CharSequence)} for
+ * each {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} received - if it
+ * doesn't, the request will eventually time out and be discarded by the Android System.
+ *
+ * <a name="SavingUserData"></a>
+ * <h3>Saving user data</h3>
+ *
+ * <p>If the service is also interested on saving the data filled by the user, it must set a
+ * {@link SaveInfo} object in the {@link FillResponse}. See {@link SaveInfo} for more details and
+ * examples.
+ *
+ * <a name="UserAuthentication"></a>
+ * <h3>User authentication</h3>
+ *
+ * <p>The service can provide an extra degree of security by requiring the user to authenticate
+ * before an app can be autofilled. The authentication is typically required in 2 scenarios:
+ * <ul>
+ * <li>To unlock the user data (for example, using a master password or fingerprint
+ * authentication) - see
+ * {@link FillResponse.Builder#setAuthentication(AutofillId[], android.content.IntentSender, android.widget.RemoteViews)}.
+ * <li>To unlock a specific dataset (for example, by providing a CVC for a credit card) - see
+ * {@link Dataset.Builder#setAuthentication(android.content.IntentSender)}.
+ * </ul>
+ *
+ * <p>When using authentication, it is recommended to encrypt only the sensitive data and leave
+ * labels unencrypted, so they can be used on presentation views. For example, if the user has a
+ * home and a work address, the {@code Home} and {@code Work} labels should be stored unencrypted
+ * (since they don't have any sensitive data) while the address data per se could be stored in an
+ * encrypted storage. Then when the user chooses the {@code Home} dataset, the platform starts
+ * the authentication flow, and the service can decrypt the sensitive data.
+ *
+ * <p>The authentication mechanism can also be used in scenarios where the service needs multiple
+ * steps to determine the datasets that can fill a screen. For example, when autofilling a financial
+ * app where the user has accounts for multiple banks, the workflow could be:
+ *
+ * <ol>
+ * <li>The first {@link FillResponse} contains datasets with the credentials for the financial
+ * app, plus a "fake" dataset whose presentation says "Tap here for banking apps credentials".
+ * <li>When the user selects the fake dataset, the service displays a dialog with available
+ * banking apps.
+ * <li>When the user select a banking app, the service replies with a new {@link FillResponse}
+ * containing the datasets for that bank.
+ * </ol>
+ *
+ * <p>Another example of multiple-steps dataset selection is when the service stores the user
+ * credentials in "vaults": the first response would contain fake datasets with the vault names,
+ * and the subsequent response would contain the app credentials stored in that vault.
+ *
+ * <a name="DataPartioning"></a>
+ * <h3>Data partitioning</h3>
+ *
+ * <p>The autofillable views in a screen should be grouped in logical groups called "partitions".
+ * Typical partitions are:
+ * <ul>
+ * <li>Credentials (username/email address, password).
+ * <li>Address (street, city, state, zip code, etc).
+ * <li>Payment info (credit card number, expiration date, and verification code).
+ * </ul>
+ * <p>For security reasons, when a screen has more than one partition, it's paramount that the
+ * contents of a dataset do not spawn multiple partitions, specially when one of the partitions
+ * contains data that is not specific to the application being autofilled. For example, a dataset
+ * should not contain fields for username, password, and credit card information. The reason for
+ * this rule is that a malicious app could draft a view structure where the credit card fields
+ * are not visible, so when the user selects a dataset from the username UI, the credit card info is
+ * released to the application without the user knowledge. Similarly, it's recommended to always
+ * protect a dataset that contains sensitive information by requiring dataset authentication
+ * (see {@link Dataset.Builder#setAuthentication(android.content.IntentSender)}), and to include
+ * info about the "primary" field of the partition in the custom presentation for "secondary"
+ * fields—that would prevent a malicious app from getting the "primary" fields without the
+ * user realizing they're being released (for example, a malicious app could have fields for a
+ * credit card number, verification code, and expiration date crafted in a way that just the latter
+ * is visible; by explicitly indicating the expiration date is related to a given credit card
+ * number, the service would be providing a visual clue for the users to check what would be
+ * released upon selecting that field).
+ *
+ * <p>When the service detects that a screen has multiple partitions, it should return a
+ * {@link FillResponse} with just the datasets for the partition that originated the request (i.e.,
+ * the partition that has the {@link android.app.assist.AssistStructure.ViewNode} whose
+ * {@link android.app.assist.AssistStructure.ViewNode#isFocused()} returns {@code true}); then if
+ * the user selects a field from a different partition, the Android System will make another
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} call for that partition,
+ * and so on.
+ *
+ * <p>Notice that when the user autofill a partition with the data provided by the service and the
+ * user did not change these fields, the autofilled value is sent back to the service in the
+ * subsequent calls (and can be obtained by calling
+ * {@link android.app.assist.AssistStructure.ViewNode#getAutofillValue()}). This is useful in the
+ * cases where the service must create datasets for a partition based on the choice made in a
+ * previous partition. For example, the 1st response for a screen that have credentials and address
+ * partitions could be:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder() // partition 1 (credentials)
+ * .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer"))
+ * .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer"))
+ * .build())
+ * .addDataset(new Dataset.Builder() // partition 1 (credentials)
+ * .setValue(id1, AutofillValue.forText("flanders"), createPresentation("flanders"))
+ * .setValue(id2, AutofillValue.forText("OkelyDokelyDo"), createPresentation("password for flanders"))
+ * .build())
+ * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD,
+ * new AutofillId[] { id1, id2 })
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>Then if the user selected {@code flanders}, the service would get a new
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} call, with the values of
+ * the fields {@code id1} and {@code id2} prepopulated, so the service could then fetch the address
+ * for the Flanders account and return the following {@link FillResponse} for the address partition:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder() // partition 2 (address)
+ * .setValue(id3, AutofillValue.forText("744 Evergreen Terrace"), createPresentation("744 Evergreen Terrace")) // street
+ * .setValue(id4, AutofillValue.forText("Springfield"), createPresentation("Springfield")) // city
+ * .build())
+ * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD | SaveInfo.SAVE_DATA_TYPE_ADDRESS,
+ * new AutofillId[] { id1, id2 }) // username and password
+ * .setOptionalIds(new AutofillId[] { id3, id4 }) // state and zipcode
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>When the service returns multiple {@link FillResponse}, the last one overrides the previous;
+ * that's why the {@link SaveInfo} in the 2nd request above has the info for both partitions.
+ *
+ * <a name="PackageVerification"></a>
+ * <h3>Package verification</h3>
+ *
+ * <p>When autofilling app-specific data (like username and password), the service must verify
+ * the authenticity of the request by obtaining all signing certificates of the app being
+ * autofilled, and only fulfilling the request when they match the values that were
+ * obtained when the data was first saved — such verification is necessary to avoid phishing
+ * attempts by apps that were sideloaded in the device with the same package name of another app.
+ * Here's an example on how to achieve that by hashing the signing certificates:
+ *
+ * <pre class="prettyprint">
+ * private String getCertificatesHash(String packageName) throws Exception {
+ * PackageManager pm = mContext.getPackageManager();
+ * PackageInfo info = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
+ * ArrayList<String> hashes = new ArrayList<>(info.signatures.length);
+ * for (Signature sig : info.signatures) {
+ * byte[] cert = sig.toByteArray();
+ * MessageDigest md = MessageDigest.getInstance("SHA-256");
+ * md.update(cert);
+ * hashes.add(toHexString(md.digest()));
+ * }
+ * Collections.sort(hashes);
+ * StringBuilder hash = new StringBuilder();
+ * for (int i = 0; i < hashes.size(); i++) {
+ * hash.append(hashes.get(i));
+ * }
+ * return hash.toString();
+ * }
+ * </pre>
+ *
+ * <p>If the service did not store the signing certificates data the first time the data was saved
+ * — for example, because the data was created by a previous version of the app that did not
+ * use the Autofill Framework — the service should warn the user that the authenticity of the
+ * app cannot be confirmed (see an example on how to show such warning in the
+ * <a href="#WebSecurityDisclaimer">Web security</a> section below), and if the user agrees,
+ * then the service could save the data from the signing ceriticates for future use.
+ *
+ * <a name="IgnoringViews"></a>
+ * <h3>Ignoring views</h3>
+ *
+ * <p>If the service find views that cannot be autofilled (for example, a text field representing
+ * the response to a Captcha challenge), it should mark those views as ignored by
+ * calling {@link FillResponse.Builder#setIgnoredIds(AutofillId...)} so the system does not trigger
+ * a new {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} when these views are
+ * focused.
+ *
+ * <a name="WebSecurity"></a>
+ * <h3>Web security</h3>
+ *
+ * <p>When handling autofill requests that represent web pages (typically
+ * view structures whose root's {@link android.app.assist.AssistStructure.ViewNode#getClassName()}
+ * is a {@link android.webkit.WebView}), the service should take the following steps to verify if
+ * the structure can be autofilled with the data associated with the app requesting it:
+ *
+ * <ol>
+ * <li>Use the {@link android.app.assist.AssistStructure.ViewNode#getWebDomain()} to get the
+ * source of the document.
+ * <li>Get the canonical domain using the
+ * <a href="https://publicsuffix.org/">Public Suffix List</a> (see example below).
+ * <li>Use <a href="https://developers.google.com/digital-asset-links/">Digital Asset Links</a>
+ * to obtain the package name and certificate fingerprint of the package corresponding to
+ * the canonical domain.
+ * <li>Make sure the certificate fingerprint matches the value returned by Package Manager
+ * (see "Package verification" section above).
+ * </ol>
+ *
+ * <p>Here's an example on how to get the canonical domain using
+ * <a href="https://github.com/google/guava">Guava</a>:
+ *
+ * <pre class="prettyprint">
+ * private static String getCanonicalDomain(String domain) {
+ * InternetDomainName idn = InternetDomainName.from(domain);
+ * while (idn != null && !idn.isTopPrivateDomain()) {
+ * idn = idn.parent();
+ * }
+ * return idn == null ? null : idn.toString();
+ * }
+ * </pre>
+ *
+ * <a name="WebSecurityDisclaimer"></a>
+ * <p>If the association between the web domain and app package cannot be verified through the steps
+ * above, but the service thinks that it is appropriate to fill persisted credentials that are
+ * stored for the web domain, the service should warn the user about the potential data
+ * leakage first, and ask for the user to confirm. For example, the service could:
+ *
+ * <ol>
+ * <li>Create a dataset that requires
+ * {@link Dataset.Builder#setAuthentication(android.content.IntentSender) authentication} to
+ * unlock.
+ * <li>Include the web domain in the custom presentation for the
+ * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, android.widget.RemoteViews)
+ * dataset value}.
+ * <li>When the user selects that dataset, show a disclaimer dialog explaining that the app is
+ * requesting credentials for a web domain, but the service could not verify if the app owns
+ * that domain. If the user agrees, then the service can unlock the dataset.
+ * <li>Similarly, when adding a {@link SaveInfo} object for the request, the service should
+ * include the above disclaimer in the {@link SaveInfo.Builder#setDescription(CharSequence)}.
+ * </ol>
+ *
+ * <p>This same procedure could also be used when the autofillable data is contained inside an
+ * {@code IFRAME}, in which case the WebView generates a new autofill context when a node inside
+ * the {@code IFRAME} is focused, with the root node containing the {@code IFRAME}'s {@code src}
+ * attribute on {@link android.app.assist.AssistStructure.ViewNode#getWebDomain()}. A typical and
+ * legitimate use case for this scenario is a financial app that allows the user
+ * to login on different bank accounts. For example, a financial app {@code my_financial_app} could
+ * use a WebView that loads contents from {@code banklogin.my_financial_app.com}, which contains an
+ * {@code IFRAME} node whose {@code src} attribute is {@code login.some_bank.com}. When fulfilling
+ * that request, the service could add an
+ * {@link Dataset.Builder#setAuthentication(android.content.IntentSender) authenticated dataset}
+ * whose presentation displays "Username for some_bank.com" and
+ * "Password for some_bank.com". Then when the user taps one of these options, the service
+ * shows the disclaimer dialog explaining that selecting that option would release the
+ * {@code login.some_bank.com} credentials to the {@code my_financial_app}; if the user agrees,
+ * then the service returns an unlocked dataset with the {@code some_bank.com} credentials.
+ *
+ * <p><b>Note:</b> The autofill service could also whitelist well-known browser apps and skip the
+ * verifications above, as long as the service can verify the authenticity of the browser app by
+ * checking its signing certificate.
+ *
+ * <a name="MultipleStepsSave"></a>
+ * <h3>Saving when data is split in multiple screens</h3>
+ *
+ * Apps often split the user data in multiple screens in the same activity, specially in
+ * activities used to create a new user account. For example, the first screen asks for a username,
+ * and if the username is available, it moves to a second screen, which asks for a password.
+ *
+ * <p>It's tricky to handle save for autofill in these situations, because the autofill service must
+ * wait until the user enters both fields before the autofill save UI can be shown. But it can be
+ * done by following the steps below:
+ *
+ * <ol>
+ * <li>In the first
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback) fill request}, the service
+ * adds a {@link FillResponse.Builder#setClientState(android.os.Bundle) client state bundle} in
+ * the response, containing the autofill ids of the partial fields present in the screen.
+ * <li>In the second
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback) fill request}, the service
+ * retrieves the {@link FillRequest#getClientState() client state bundle}, gets the autofill ids
+ * set in the previous request from the client state, and adds these ids and the
+ * {@link SaveInfo#FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} to the {@link SaveInfo} used in the second
+ * response.
+ * <li>In the {@link #onSaveRequest(SaveRequest, SaveCallback) save request}, the service uses the
+ * proper {@link FillContext fill contexts} to get the value of each field (there is one fill
+ * context per fill request).
+ * </ol>
+ *
+ * <p>For example, in an app that uses 2 steps for the username and password fields, the workflow
+ * would be:
+ * <pre class="prettyprint">
+ * // On first fill request
+ * AutofillId usernameId = // parse from AssistStructure;
+ * Bundle clientState = new Bundle();
+ * clientState.putParcelable("usernameId", usernameId);
+ * fillCallback.onSuccess(
+ * new FillResponse.Builder()
+ * .setClientState(clientState)
+ * .setSaveInfo(new SaveInfo
+ * .Builder(SaveInfo.SAVE_DATA_TYPE_USERNAME, new AutofillId[] {usernameId})
+ * .build())
+ * .build());
+ *
+ * // On second fill request
+ * Bundle clientState = fillRequest.getClientState();
+ * AutofillId usernameId = clientState.getParcelable("usernameId");
+ * AutofillId passwordId = // parse from AssistStructure
+ * clientState.putParcelable("passwordId", passwordId);
+ * fillCallback.onSuccess(
+ * new FillResponse.Builder()
+ * .setClientState(clientState)
+ * .setSaveInfo(new SaveInfo
+ * .Builder(SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD,
+ * new AutofillId[] {usernameId, passwordId})
+ * .setFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE)
+ * .build())
+ * .build());
+ *
+ * // On save request
+ * Bundle clientState = saveRequest.getClientState();
+ * AutofillId usernameId = clientState.getParcelable("usernameId");
+ * AutofillId passwordId = clientState.getParcelable("passwordId");
+ * List<FillContext> fillContexts = saveRequest.getFillContexts();
+ *
+ * FillContext usernameContext = fillContexts.get(0);
+ * ViewNode usernameNode = findNodeByAutofillId(usernameContext.getStructure(), usernameId);
+ * AutofillValue username = usernameNode.getAutofillValue().getTextValue().toString();
+ *
+ * FillContext passwordContext = fillContexts.get(1);
+ * ViewNode passwordNode = findNodeByAutofillId(passwordContext.getStructure(), passwordId);
+ * AutofillValue password = passwordNode.getAutofillValue().getTextValue().toString();
+ *
+ * save(username, password);
+ * </pre>
+ *
+ * <a name="Privacy"></a>
+ * <h3>Privacy</h3>
+ *
+ * <p>The {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} method is called
+ * without the user content. The Android system strips some properties of the
+ * {@link android.app.assist.AssistStructure.ViewNode view nodes} passed to this call, but not all
+ * of them. For example, the data provided in the {@link android.view.ViewStructure.HtmlInfo}
+ * objects set by {@link android.webkit.WebView} is never stripped out.
+ *
+ * <p>Because this data could contain PII (Personally Identifiable Information, such as username or
+ * email address), the service should only use it locally (i.e., in the app's process) for
+ * heuristics purposes, but it should not be sent to external servers.
+ *
+ * <a name="FieldClassification"></a>
+ * <h3>Metrics and field classification</h3
+ *
+ * <p>The service can call {@link #getFillEventHistory()} to get metrics representing the user
+ * actions, and then use these metrics to improve its heuristics.
+ *
+ * <p>Prior to Android {@link android.os.Build.VERSION_CODES#P}, the metrics covered just the
+ * scenarios where the service knew how to autofill an activity, but Android
+ * {@link android.os.Build.VERSION_CODES#P} introduced a new mechanism called field classification,
+ * which allows the service to dinamically classify the meaning of fields based on the existing user
+ * data known by the service.
+ *
+ * <p>Typically, field classification can be used to detect fields that can be autofilled with
+ * user data that is not associated with a specific app—such as email and physical
+ * address. Once the service identifies that a such field was manually filled by the user, the
+ * service could use this signal to improve its heuristics on subsequent requests (for example, by
+ * infering which resource ids are associated with known fields).
+ *
+ * <p>The field classification workflow involves 4 steps:
+ *
+ * <ol>
+ * <li>Set the user data through {@link AutofillManager#setUserData(UserData)}. This data is
+ * cached until the system restarts (or the service is disabled), so it doesn't need to be set for
+ * all requests.
+ * <li>Identify which fields should be analysed by calling
+ * {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)}.
+ * <li>Verify the results through {@link FillEventHistory.Event#getFieldsClassification()}.
+ * <li>Use the results to dynamically create {@link Dataset} or {@link SaveInfo} objects in
+ * subsequent requests.
+ * </ol>
+ *
+ * <p>The field classification is an expensive operation and should be used carefully, otherwise it
+ * can reach its rate limit and get blocked by the Android System. Ideally, it should be used just
+ * in cases where the service could not determine how an activity can be autofilled, but it has a
+ * strong suspicious that it could. For example, if an activity has four or more fields and one of
+ * them is a list, chances are that these are address fields (like address, city, state, and
+ * zip code).
+ *
+ * <a name="CompatibilityMode"></a>
+ * <h3>Compatibility mode</h3>
+ *
+ * <p>Apps that use standard Android widgets support autofill out-of-the-box and need to do
+ * very little to improve their user experience (annotating autofillable views and providing
+ * autofill hints). However, some apps (typically browsers) do their own rendering and the rendered
+ * content may contain semantic structure that needs to be surfaced to the autofill framework. The
+ * platform exposes APIs to achieve this, however it could take some time until these apps implement
+ * autofill support.
+ *
+ * <p>To enable autofill for such apps the platform provides a compatibility mode in which the
+ * platform would fall back to the accessibility APIs to generate the state reported to autofill
+ * services and fill data. This mode needs to be explicitly requested for a given package up
+ * to a specified max version code allowing clean migration path when the target app begins to
+ * support autofill natively. Note that enabling compatibility may degrade performance for the
+ * target package and should be used with caution. The platform supports whitelisting which packages
+ * can be targeted in compatibility mode to ensure this mode is used only when needed and as long
+ * as needed.
+ *
+ * <p>You can request compatibility mode for packages of interest in the meta-data resource
+ * associated with your service. Below is a sample service declaration:
+ *
+ * <pre> <service android:name=".MyAutofillService"
+ * android:permission="android.permission.BIND_AUTOFILL_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.autofill.AutofillService" />
+ * </intent-filter>
+ * <meta-data android:name="android.autofill" android:resource="@xml/autofillservice" />
+ * </service></pre>
+ *
+ * <p>In the XML file you can specify one or more packages for which to enable compatibility
+ * mode. Below is a sample meta-data declaration:
+ *
+ * <pre> <autofill-service xmlns:android="http://schemas.android.com/apk/res/android">
+ * <compatibility-package android:name="foo.bar.baz" android:maxLongVersionCode="1000000000"/>
+ * </autofill-service></pre>
+ *
+ * <p>Notice that compatibility mode has limitations such as:
+ * <ul>
+ * <li>No manual autofill requests. Hence, the {@link FillRequest}
+ * {@link FillRequest#getFlags() flags} never have the {@link FillRequest#FLAG_MANUAL_REQUEST} flag.
+ * <li>The value of password fields are most likely masked—for example, {@code ****} instead
+ * of {@code 1234}. Hence, you must be careful when using these values to avoid updating the user
+ * data with invalid input. For example, when you parse the {@link FillRequest} and detect a
+ * password field, you could check if its
+ * {@link android.app.assist.AssistStructure.ViewNode#getInputType()
+ * input type} has password flags and if so, don't add it to the {@link SaveInfo} object.
+ * <li>The autofill context is not always {@link AutofillManager#commit() committed} when an HTML
+ * form is submitted. Hence, you must use other mechanisms to trigger save, such as setting the
+ * {@link SaveInfo#FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} flag on {@link SaveInfo.Builder#setFlags(int)}
+ * or using {@link SaveInfo.Builder#setTriggerId(AutofillId)}.
+ * <li>Browsers often provide their own autofill management system. When both the browser and
+ * the platform render an autofill dialog at the same time, the result can be confusing to the user.
+ * Such browsers typically offer an option for users to disable autofill, so your service should
+ * also allow users to disable compatiblity mode for specific apps. That way, it is up to the user
+ * to decide which autofill mechanism—the browser's or the platform's—should be used.
+ * </ul>
+ */
+public abstract class AutofillService extends Service {
+ private static final String TAG = "AutofillService";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_AUTOFILL_SERVICE} permission so
+ * that other applications can not abuse it.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.service.autofill.AutofillService";
+
+ /**
+ * Name under which a AutoFillService component publishes information about itself.
+ * This meta-data should reference an XML resource containing a
+ * <code><{@link
+ * android.R.styleable#AutofillService autofill-service}></code> tag.
+ * This is a a sample XML file configuring an AutoFillService:
+ * <pre> <autofill-service
+ * android:settingsActivity="foo.bar.SettingsActivity"
+ * . . .
+ * /></pre>
+ */
+ public static final String SERVICE_META_DATA = "android.autofill";
+
+ private final IAutoFillService mInterface = new IAutoFillService.Stub() {
+ @Override
+ public void onConnectedStateChanged(boolean connected) {
+ mHandler.sendMessage(obtainMessage(
+ connected ? AutofillService::onConnected : AutofillService::onDisconnected,
+ AutofillService.this));
+ }
+
+ @Override
+ public void onFillRequest(FillRequest request, IFillCallback callback) {
+ ICancellationSignal transport = CancellationSignal.createTransport();
+ try {
+ callback.onCancellable(transport);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ mHandler.sendMessage(obtainMessage(
+ AutofillService::onFillRequest,
+ AutofillService.this, request, CancellationSignal.fromTransport(transport),
+ new FillCallback(callback, request.getId())));
+ }
+
+ @Override
+ public void onSaveRequest(SaveRequest request, ISaveCallback callback) {
+ mHandler.sendMessage(obtainMessage(
+ AutofillService::onSaveRequest,
+ AutofillService.this, request, new SaveCallback(callback)));
+ }
+ };
+
+ private Handler mHandler;
+
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler(Looper.getMainLooper(), null, true);
+ BaseBundle.setShouldDefuse(true);
+ }
+
+ @Override
+ public final IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mInterface.asBinder();
+ }
+ Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+ return null;
+ }
+
+ /**
+ * Called when the Android system connects to service.
+ *
+ * <p>You should generally do initialization here rather than in {@link #onCreate}.
+ */
+ public void onConnected() {
+ }
+
+ /**
+ * Called by the Android system do decide if a screen can be autofilled by the service.
+ *
+ * <p>Service must call one of the {@link FillCallback} methods (like
+ * {@link FillCallback#onSuccess(FillResponse)}
+ * or {@link FillCallback#onFailure(CharSequence)})
+ * to notify the result of the request.
+ *
+ * @param request the {@link FillRequest request} to handle.
+ * See {@link FillResponse} for examples of multiple-sections requests.
+ * @param cancellationSignal signal for observing cancellation requests. The system will use
+ * this to notify you that the fill result is no longer needed and you should stop
+ * handling this fill request in order to save resources.
+ * @param callback object used to notify the result of the request.
+ */
+ public abstract void onFillRequest(@NonNull FillRequest request,
+ @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback);
+
+ /**
+ * Called when the user requests the service to save the contents of a screen.
+ *
+ * <p>If the service could not handle the request right away—for example, because it must
+ * launch an activity asking the user to authenticate first or because the network is
+ * down—the service could keep the {@link SaveRequest request} and reuse it later,
+ * but the service <b>must always</b> call {@link SaveCallback#onSuccess()} or
+ * {@link SaveCallback#onSuccess(android.content.IntentSender)} right away.
+ *
+ * <p><b>Note:</b> To retrieve the actual value of fields input by the user, the service
+ * should call
+ * {@link android.app.assist.AssistStructure.ViewNode#getAutofillValue()}; if it calls
+ * {@link android.app.assist.AssistStructure.ViewNode#getText()} or other methods, there is no
+ * guarantee such method will return the most recent value of the field.
+ *
+ * @param request the {@link SaveRequest request} to handle.
+ * See {@link FillResponse} for examples of multiple-sections requests.
+ * @param callback object used to notify the result of the request.
+ */
+ public abstract void onSaveRequest(@NonNull SaveRequest request,
+ @NonNull SaveCallback callback);
+
+ /**
+ * Called when the Android system disconnects from the service.
+ *
+ * <p> At this point this service may no longer be an active {@link AutofillService}.
+ * It should not make calls on {@link AutofillManager} that requires the caller to be
+ * the current service.
+ */
+ public void onDisconnected() {
+ }
+
+ /**
+ * Gets the events that happened after the last
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}
+ * call.
+ *
+ * <p>This method is typically used to keep track of previous user actions to optimize further
+ * requests. For example, the service might return email addresses in alphabetical order by
+ * default, but change that order based on the address the user picked on previous requests.
+ *
+ * <p>The history is not persisted over reboots, and it's cleared every time the service
+ * replies to a {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} by calling
+ * {@link FillCallback#onSuccess(FillResponse)} or {@link FillCallback#onFailure(CharSequence)}
+ * (if the service doesn't call any of these methods, the history will clear out after some
+ * pre-defined time). Hence, the service should call {@link #getFillEventHistory()} before
+ * finishing the {@link FillCallback}.
+ *
+ * @return The history or {@code null} if there are no events.
+ *
+ * @throws RuntimeException if the event history could not be retrieved.
+ */
+ @Nullable public final FillEventHistory getFillEventHistory() {
+ final AutofillManager afm = getSystemService(AutofillManager.class);
+
+ if (afm == null) {
+ return null;
+ } else {
+ return afm.getFillEventHistory();
+ }
+ }
+}
diff --git a/android/service/autofill/AutofillServiceHelper.java b/android/service/autofill/AutofillServiceHelper.java
new file mode 100644
index 0000000..13fedba
--- /dev/null
+++ b/android/service/autofill/AutofillServiceHelper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 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.autofill;
+
+import android.annotation.Nullable;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.util.Preconditions;
+
+/** @hide */
+final class AutofillServiceHelper {
+
+ static AutofillId[] assertValid(@Nullable AutofillId[] ids) {
+ Preconditions.checkArgument(ids != null && ids.length > 0, "must have at least one id");
+ // Can't use Preconditions.checkArrayElementsNotNull() because it throws NPE instead of IAE
+ for (int i = 0; i < ids.length; ++i) {
+ if (ids[i] == null) {
+ throw new IllegalArgumentException("ids[" + i + "] must not be null");
+ }
+ }
+ return ids;
+ }
+
+ private AutofillServiceHelper() {
+ throw new UnsupportedOperationException("contains static members only");
+ }
+}
diff --git a/android/service/autofill/AutofillServiceInfo.java b/android/service/autofill/AutofillServiceInfo.java
new file mode 100644
index 0000000..fbc25a6
--- /dev/null
+++ b/android/service/autofill/AutofillServiceInfo.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2016 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.autofill;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.metrics.LogMaker;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.internal.R;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * {@link ServiceInfo} and meta-data about an {@link AutofillService}.
+ *
+ * @hide
+ */
+public final class AutofillServiceInfo {
+ private static final String TAG = "AutofillServiceInfo";
+
+ private static final String TAG_AUTOFILL_SERVICE = "autofill-service";
+ private static final String TAG_COMPATIBILITY_PACKAGE = "compatibility-package";
+
+ private static ServiceInfo getServiceInfoOrThrow(ComponentName comp, int userHandle)
+ throws PackageManager.NameNotFoundException {
+ try {
+ ServiceInfo si = AppGlobals.getPackageManager().getServiceInfo(
+ comp,
+ PackageManager.GET_META_DATA,
+ userHandle);
+ if (si != null) {
+ return si;
+ }
+ } catch (RemoteException e) {
+ }
+ throw new PackageManager.NameNotFoundException(comp.toString());
+ }
+
+ @NonNull
+ private final ServiceInfo mServiceInfo;
+
+ @Nullable
+ private final String mSettingsActivity;
+
+ @Nullable
+ private final ArrayMap<String, Long> mCompatibilityPackages;
+
+ private final boolean mInlineSuggestionsEnabled;
+
+ public AutofillServiceInfo(Context context, ComponentName comp, int userHandle)
+ throws PackageManager.NameNotFoundException {
+ this(context, getServiceInfoOrThrow(comp, userHandle));
+ }
+
+ public AutofillServiceInfo(Context context, ServiceInfo si) {
+ // Check for permissions.
+ if (!Manifest.permission.BIND_AUTOFILL_SERVICE.equals(si.permission)) {
+ if (Manifest.permission.BIND_AUTOFILL.equals(si.permission)) {
+ // Let it go for now...
+ Log.w(TAG, "AutofillService from '" + si.packageName + "' uses unsupported "
+ + "permission " + Manifest.permission.BIND_AUTOFILL + ". It works for "
+ + "now, but might not be supported on future releases");
+ new MetricsLogger().write(new LogMaker(MetricsEvent.AUTOFILL_INVALID_PERMISSION)
+ .setPackageName(si.packageName));
+ } else {
+ Log.w(TAG, "AutofillService from '" + si.packageName
+ + "' does not require permission "
+ + Manifest.permission.BIND_AUTOFILL_SERVICE);
+ throw new SecurityException("Service does not require permission "
+ + Manifest.permission.BIND_AUTOFILL_SERVICE);
+ }
+ }
+
+ mServiceInfo = si;
+
+ // Get the AutoFill metadata, if declared.
+ final XmlResourceParser parser = si.loadXmlMetaData(context.getPackageManager(),
+ AutofillService.SERVICE_META_DATA);
+ if (parser == null) {
+ mSettingsActivity = null;
+ mCompatibilityPackages = null;
+ mInlineSuggestionsEnabled = false;
+ return;
+ }
+
+ String settingsActivity = null;
+ ArrayMap<String, Long> compatibilityPackages = null;
+ boolean inlineSuggestionsEnabled = false; // false by default.
+
+ try {
+ final Resources resources = context.getPackageManager().getResourcesForApplication(
+ si.applicationInfo);
+
+ int type = 0;
+ while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
+ type = parser.next();
+ }
+
+ if (TAG_AUTOFILL_SERVICE.equals(parser.getName())) {
+ final AttributeSet allAttributes = Xml.asAttributeSet(parser);
+ TypedArray afsAttributes = null;
+ try {
+ afsAttributes = resources.obtainAttributes(allAttributes,
+ com.android.internal.R.styleable.AutofillService);
+ settingsActivity = afsAttributes.getString(
+ R.styleable.AutofillService_settingsActivity);
+ inlineSuggestionsEnabled = afsAttributes.getBoolean(
+ R.styleable.AutofillService_supportsInlineSuggestions, false);
+ } finally {
+ if (afsAttributes != null) {
+ afsAttributes.recycle();
+ }
+ }
+ compatibilityPackages = parseCompatibilityPackages(parser, resources);
+ } else {
+ Log.e(TAG, "Meta-data does not start with autofill-service tag");
+ }
+ } catch (PackageManager.NameNotFoundException | IOException | XmlPullParserException e) {
+ Log.e(TAG, "Error parsing auto fill service meta-data", e);
+ }
+
+ mSettingsActivity = settingsActivity;
+ mCompatibilityPackages = compatibilityPackages;
+ mInlineSuggestionsEnabled = inlineSuggestionsEnabled;
+ }
+
+ private ArrayMap<String, Long> parseCompatibilityPackages(XmlPullParser parser,
+ Resources resources) throws IOException, XmlPullParserException {
+ ArrayMap<String, Long> compatibilityPackages = null;
+
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+
+ if (TAG_COMPATIBILITY_PACKAGE.equals(parser.getName())) {
+ TypedArray cpAttributes = null;
+ try {
+ final AttributeSet allAttributes = Xml.asAttributeSet(parser);
+
+ cpAttributes = resources.obtainAttributes(allAttributes,
+ R.styleable.AutofillService_CompatibilityPackage);
+
+ final String name = cpAttributes.getString(
+ R.styleable.AutofillService_CompatibilityPackage_name);
+ if (TextUtils.isEmpty(name)) {
+ Log.e(TAG, "Invalid compatibility package:" + name);
+ break;
+ }
+
+ final String maxVersionCodeStr = cpAttributes.getString(
+ R.styleable.AutofillService_CompatibilityPackage_maxLongVersionCode);
+ final Long maxVersionCode;
+ if (maxVersionCodeStr != null) {
+ try {
+ maxVersionCode = Long.parseLong(maxVersionCodeStr);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Invalid compatibility max version code:"
+ + maxVersionCodeStr);
+ break;
+ }
+ if (maxVersionCode < 0) {
+ Log.e(TAG, "Invalid compatibility max version code:"
+ + maxVersionCode);
+ break;
+ }
+ } else {
+ maxVersionCode = Long.MAX_VALUE;
+ }
+ if (compatibilityPackages == null) {
+ compatibilityPackages = new ArrayMap<>();
+ }
+ compatibilityPackages.put(name, maxVersionCode);
+ } finally {
+ XmlUtils.skipCurrentTag(parser);
+ if (cpAttributes != null) {
+ cpAttributes.recycle();
+ }
+ }
+ }
+ }
+
+ return compatibilityPackages;
+ }
+
+ public ServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ @Nullable
+ public String getSettingsActivity() {
+ return mSettingsActivity;
+ }
+
+ public ArrayMap<String, Long> getCompatibilityPackages() {
+ return mCompatibilityPackages;
+ }
+
+ public boolean isInlineSuggestionsEnabled() {
+ return mInlineSuggestionsEnabled;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(getClass().getSimpleName());
+ builder.append("[").append(mServiceInfo);
+ builder.append(", settings:").append(mSettingsActivity);
+ builder.append(", hasCompatPckgs:").append(mCompatibilityPackages != null
+ && !mCompatibilityPackages.isEmpty()).append("]");
+ builder.append(", inline suggestions enabled:").append(mInlineSuggestionsEnabled);
+ return builder.toString();
+ }
+
+ /**
+ * Dumps it!
+ */
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("Component: "); pw.println(getServiceInfo().getComponentName());
+ pw.print(prefix); pw.print("Settings: "); pw.println(mSettingsActivity);
+ pw.print(prefix); pw.print("Compat packages: "); pw.println(mCompatibilityPackages);
+ pw.print(prefix); pw.print("Inline Suggestions Enabled: ");
+ pw.println(mInlineSuggestionsEnabled);
+ }
+}
diff --git a/android/service/autofill/BatchUpdates.java b/android/service/autofill/BatchUpdates.java
new file mode 100644
index 0000000..e0b1c2f
--- /dev/null
+++ b/android/service/autofill/BatchUpdates.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+
+/**
+ * Defines actions to be applied to a {@link RemoteViews template presentation}.
+ *
+ *
+ * <p>It supports 2 types of actions:
+ *
+ * <ol>
+ * <li>{@link RemoteViews Actions} to be applied to the template.
+ * <li>{@link Transformation Transformations} to be applied on child views.
+ * </ol>
+ *
+ * <p>Typically used on {@link CustomDescription custom descriptions} to conditionally display
+ * differents views based on user input - see
+ * {@link CustomDescription.Builder#batchUpdate(Validator, BatchUpdates)} for more information.
+ */
+public final class BatchUpdates implements Parcelable {
+
+ private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
+ private final RemoteViews mUpdates;
+
+ private BatchUpdates(Builder builder) {
+ mTransformations = builder.mTransformations;
+ mUpdates = builder.mUpdates;
+ }
+
+ /** @hide */
+ @Nullable
+ public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() {
+ return mTransformations;
+ }
+
+ /** @hide */
+ @Nullable
+ public RemoteViews getUpdates() {
+ return mUpdates;
+ }
+
+ /**
+ * Builder for {@link BatchUpdates} objects.
+ */
+ public static class Builder {
+ private RemoteViews mUpdates;
+
+ private boolean mDestroyed;
+ private ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
+
+ /**
+ * Applies the {@code updates} in the underlying presentation template.
+ *
+ * <p><b>Note:</b> The updates are applied before the
+ * {@link #transformChild(int, Transformation) transformations} are applied to the children
+ * views.
+ *
+ * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
+ * or background color: Autofill on different platforms may have different themes.
+ *
+ * @param updates a {@link RemoteViews} with the updated actions to be applied in the
+ * underlying presentation template.
+ *
+ * @return this builder
+ * @throws IllegalArgumentException if {@code condition} is not a class provided
+ * by the Android System.
+ */
+ public Builder updateTemplate(@NonNull RemoteViews updates) {
+ throwIfDestroyed();
+ mUpdates = Preconditions.checkNotNull(updates);
+ return this;
+ }
+
+ /**
+ * Adds a transformation to replace the value of a child view with the fields in the
+ * screen.
+ *
+ * <p>When multiple transformations are added for the same child view, they are applied
+ * in the same order as added.
+ *
+ * <p><b>Note:</b> The transformations are applied after the
+ * {@link #updateTemplate(RemoteViews) updates} are applied to the presentation template.
+ *
+ * @param id view id of the children view.
+ * @param transformation an implementation provided by the Android System.
+ * @return this builder.
+ * @throws IllegalArgumentException if {@code transformation} is not a class provided
+ * by the Android System.
+ */
+ public Builder transformChild(int id, @NonNull Transformation transformation) {
+ throwIfDestroyed();
+ Preconditions.checkArgument((transformation instanceof InternalTransformation),
+ "not provided by Android System: " + transformation);
+ if (mTransformations == null) {
+ mTransformations = new ArrayList<>();
+ }
+ mTransformations.add(new Pair<>(id, (InternalTransformation) transformation));
+ return this;
+ }
+
+ /**
+ * Creates a new {@link BatchUpdates} instance.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called before or no call
+ * to {@link #updateTemplate(RemoteViews)} or {@link #transformChild(int, Transformation)}
+ * has been made.
+ */
+ public BatchUpdates build() {
+ throwIfDestroyed();
+ Preconditions.checkState(mUpdates != null || mTransformations != null,
+ "must call either updateTemplate() or transformChild() at least once");
+ mDestroyed = true;
+ return new BatchUpdates(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return new StringBuilder("BatchUpdates: [")
+ .append(", transformations=")
+ .append(mTransformations == null ? "N/A" : mTransformations.size())
+ .append(", updates=").append(mUpdates)
+ .append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mTransformations == null) {
+ dest.writeIntArray(null);
+ } else {
+ final int size = mTransformations.size();
+ final int[] ids = new int[size];
+ final InternalTransformation[] values = new InternalTransformation[size];
+ for (int i = 0; i < size; i++) {
+ final Pair<Integer, InternalTransformation> pair = mTransformations.get(i);
+ ids[i] = pair.first;
+ values[i] = pair.second;
+ }
+ dest.writeIntArray(ids);
+ dest.writeParcelableArray(values, flags);
+ }
+ dest.writeParcelable(mUpdates, flags);
+ }
+ public static final @android.annotation.NonNull Parcelable.Creator<BatchUpdates> CREATOR =
+ new Parcelable.Creator<BatchUpdates>() {
+ @Override
+ public BatchUpdates createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final Builder builder = new Builder();
+ final int[] ids = parcel.createIntArray();
+ if (ids != null) {
+ final InternalTransformation[] values =
+ parcel.readParcelableArray(null, InternalTransformation.class);
+ final int size = ids.length;
+ for (int i = 0; i < size; i++) {
+ builder.transformChild(ids[i], values[i]);
+ }
+ }
+ final RemoteViews updates = parcel.readParcelable(null);
+ if (updates != null) {
+ builder.updateTemplate(updates);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public BatchUpdates[] newArray(int size) {
+ return new BatchUpdates[size];
+ }
+ };
+}
diff --git a/android/service/autofill/CharSequenceTransformation.java b/android/service/autofill/CharSequenceTransformation.java
new file mode 100644
index 0000000..e3e8844
--- /dev/null
+++ b/android/service/autofill/CharSequenceTransformation.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.util.Pair;
+import android.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Replaces a {@link TextView} child of a {@link CustomDescription} with the contents of one or
+ * more regular expressions (regexs).
+ *
+ * <p>When it contains more than one field, the fields that match their regex are added to the
+ * overall transformation result.
+ *
+ * <p>For example, a transformation to mask a credit card number contained in just one field would
+ * be:
+ *
+ * <pre class="prettyprint">
+ * new CharSequenceTransformation
+ * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
+ * .build();
+ * </pre>
+ *
+ * <p>But a transformation that generates a {@code Exp: MM / YYYY} credit expiration date from two
+ * fields (month and year) would be:
+ *
+ * <pre class="prettyprint">
+ * new CharSequenceTransformation
+ * .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
+ * .addField(ccExpYearId, Pattern.compile("^(\\d\\d\\d\\d)$"), " / $1");
+ * </pre>
+ */
+public final class CharSequenceTransformation extends InternalTransformation implements
+ Transformation, Parcelable {
+ private static final String TAG = "CharSequenceTransformation";
+
+ // Must use LinkedHashMap to preserve insertion order.
+ @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields;
+
+ private CharSequenceTransformation(Builder builder) {
+ mFields = builder.mFields;
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
+ int childViewId) throws Exception {
+ final StringBuilder converted = new StringBuilder();
+ final int size = mFields.size();
+ if (sDebug) Log.d(TAG, size + " fields on id " + childViewId);
+ for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
+ final AutofillId id = entry.getKey();
+ final Pair<Pattern, String> field = entry.getValue();
+ final String value = finder.findByAutofillId(id);
+ if (value == null) {
+ Log.w(TAG, "No value for id " + id);
+ return;
+ }
+ try {
+ final Matcher matcher = field.first.matcher(value);
+ if (!matcher.find()) {
+ if (sDebug) Log.d(TAG, "Match for " + field.first + " failed on id " + id);
+ return;
+ }
+ // replaceAll throws an exception if the subst is invalid
+ final String convertedValue = matcher.replaceAll(field.second);
+ converted.append(convertedValue);
+ } catch (Exception e) {
+ // Do not log full exception to avoid PII leaking
+ Log.w(TAG, "Cannot apply " + field.first.pattern() + "->" + field.second + " to "
+ + "field with autofill id" + id + ": " + e.getClass());
+ throw e;
+ }
+ }
+ // Cannot log converted, it might have PII
+ Log.d(TAG, "Converting text on child " + childViewId + " to " + converted.length()
+ + "_chars");
+ parentTemplate.setCharSequence(childViewId, "setText", converted);
+ }
+
+ /**
+ * Builder for {@link CharSequenceTransformation} objects.
+ */
+ public static class Builder {
+
+ // Must use LinkedHashMap to preserve insertion order.
+ @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields =
+ new LinkedHashMap<>();
+ private boolean mDestroyed;
+
+ /**
+ * Creates a new builder and adds the first transformed contents of a field to the overall
+ * result of this transformation.
+ *
+ * @param id id of the screen field.
+ * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
+ * are used to substitute parts of the value.
+ * @param subst the string that substitutes the matched regex, using {@code $} for
+ * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
+ */
+ public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @NonNull String subst) {
+ addField(id, regex, subst);
+ }
+
+ /**
+ * Adds the transformed contents of a field to the overall result of this transformation.
+ *
+ * @param id id of the screen field.
+ * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
+ * are used to substitute parts of the value.
+ * @param subst the string that substitutes the matched regex, using {@code $} for
+ * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
+ *
+ * @return this builder.
+ */
+ public Builder addField(@NonNull AutofillId id, @NonNull Pattern regex,
+ @NonNull String subst) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(id);
+ Preconditions.checkNotNull(regex);
+ Preconditions.checkNotNull(subst);
+
+ mFields.put(id, new Pair<>(regex, subst));
+ return this;
+ }
+
+ /**
+ * Creates a new {@link CharSequenceTransformation} instance.
+ */
+ public CharSequenceTransformation build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new CharSequenceTransformation(this);
+ }
+
+ private void throwIfDestroyed() {
+ Preconditions.checkState(!mDestroyed, "Already called build()");
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "MultipleViewsCharSequenceTransformation: [fields=" + mFields + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ final int size = mFields.size();
+ final AutofillId[] ids = new AutofillId[size];
+ final Pattern[] regexs = new Pattern[size];
+ final String[] substs = new String[size];
+ Pair<Pattern, String> pair;
+ int i = 0;
+ for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
+ ids[i] = entry.getKey();
+ pair = entry.getValue();
+ regexs[i] = pair.first;
+ substs[i] = pair.second;
+ i++;
+ }
+
+ parcel.writeParcelableArray(ids, flags);
+ parcel.writeSerializable(regexs);
+ parcel.writeStringArray(substs);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<CharSequenceTransformation> CREATOR =
+ new Parcelable.Creator<CharSequenceTransformation>() {
+ @Override
+ public CharSequenceTransformation createFromParcel(Parcel parcel) {
+ final AutofillId[] ids = parcel.readParcelableArray(null, AutofillId.class);
+ final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
+ final String[] substs = parcel.createStringArray();
+
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final CharSequenceTransformation.Builder builder =
+ new CharSequenceTransformation.Builder(ids[0], regexs[0], substs[0]);
+
+ final int size = ids.length;
+ for (int i = 1; i < size; i++) {
+ builder.addField(ids[i], regexs[i], substs[i]);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public CharSequenceTransformation[] newArray(int size) {
+ return new CharSequenceTransformation[size];
+ }
+ };
+}
diff --git a/android/service/autofill/CompositeUserData.java b/android/service/autofill/CompositeUserData.java
new file mode 100644
index 0000000..c7dc15a
--- /dev/null
+++ b/android/service/autofill/CompositeUserData.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2018 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Holds both a generic and package-specific userData used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * @hide
+ */
+@TestApi
+public final class CompositeUserData implements FieldClassificationUserData, Parcelable {
+
+ private final UserData mGenericUserData;
+ private final UserData mPackageUserData;
+
+ private final String[] mCategories;
+ private final String[] mValues;
+
+ public CompositeUserData(@Nullable UserData genericUserData,
+ @NonNull UserData packageUserData) {
+ mGenericUserData = genericUserData;
+ mPackageUserData = packageUserData;
+
+ final String[] packageCategoryIds = mPackageUserData.getCategoryIds();
+ final String[] packageValues = mPackageUserData.getValues();
+
+ final ArrayList<String> categoryIds = new ArrayList<>(packageCategoryIds.length);
+ final ArrayList<String> values = new ArrayList<>(packageValues.length);
+
+ Collections.addAll(categoryIds, packageCategoryIds);
+ Collections.addAll(values, packageValues);
+
+ if (mGenericUserData != null) {
+ final String[] genericCategoryIds = mGenericUserData.getCategoryIds();
+ final String[] genericValues = mGenericUserData.getValues();
+ final int size = mGenericUserData.getCategoryIds().length;
+ for (int i = 0; i < size; i++) {
+ if (!categoryIds.contains(genericCategoryIds[i])) {
+ categoryIds.add(genericCategoryIds[i]);
+ values.add(genericValues[i]);
+ }
+ }
+ }
+
+ mCategories = new String[categoryIds.size()];
+ categoryIds.toArray(mCategories);
+ mValues = new String[values.size()];
+ values.toArray(mValues);
+ }
+
+ @Nullable
+ @Override
+ public String getFieldClassificationAlgorithm() {
+ final String packageDefaultAlgo = mPackageUserData.getFieldClassificationAlgorithm();
+ if (packageDefaultAlgo != null) {
+ return packageDefaultAlgo;
+ } else {
+ return mGenericUserData == null ? null :
+ mGenericUserData.getFieldClassificationAlgorithm();
+ }
+ }
+
+ @Override
+ public Bundle getDefaultFieldClassificationArgs() {
+ final Bundle packageDefaultArgs = mPackageUserData.getDefaultFieldClassificationArgs();
+ if (packageDefaultArgs != null) {
+ return packageDefaultArgs;
+ } else {
+ return mGenericUserData == null ? null :
+ mGenericUserData.getDefaultFieldClassificationArgs();
+ }
+ }
+
+ @Nullable
+ @Override
+ public String getFieldClassificationAlgorithmForCategory(@NonNull String categoryId) {
+ Preconditions.checkNotNull(categoryId);
+ final ArrayMap<String, String> categoryAlgorithms = getFieldClassificationAlgorithms();
+ if (categoryAlgorithms == null || !categoryAlgorithms.containsKey(categoryId)) {
+ return null;
+ }
+ return categoryAlgorithms.get(categoryId);
+ }
+
+ @Override
+ public ArrayMap<String, String> getFieldClassificationAlgorithms() {
+ final ArrayMap<String, String> packageAlgos = mPackageUserData
+ .getFieldClassificationAlgorithms();
+ final ArrayMap<String, String> genericAlgos = mGenericUserData == null ? null :
+ mGenericUserData.getFieldClassificationAlgorithms();
+
+ ArrayMap<String, String> categoryAlgorithms = null;
+ if (packageAlgos != null || genericAlgos != null) {
+ categoryAlgorithms = new ArrayMap<>();
+ if (genericAlgos != null) {
+ categoryAlgorithms.putAll(genericAlgos);
+ }
+ if (packageAlgos != null) {
+ categoryAlgorithms.putAll(packageAlgos);
+ }
+ }
+
+ return categoryAlgorithms;
+ }
+
+ @Override
+ public ArrayMap<String, Bundle> getFieldClassificationArgs() {
+ final ArrayMap<String, Bundle> packageArgs = mPackageUserData.getFieldClassificationArgs();
+ final ArrayMap<String, Bundle> genericArgs = mGenericUserData == null ? null :
+ mGenericUserData.getFieldClassificationArgs();
+
+ ArrayMap<String, Bundle> categoryArgs = null;
+ if (packageArgs != null || genericArgs != null) {
+ categoryArgs = new ArrayMap<>();
+ if (genericArgs != null) {
+ categoryArgs.putAll(genericArgs);
+ }
+ if (packageArgs != null) {
+ categoryArgs.putAll(packageArgs);
+ }
+ }
+
+ return categoryArgs;
+ }
+
+ @Override
+ public String[] getCategoryIds() {
+ return mCategories;
+ }
+
+ @Override
+ public String[] getValues() {
+ return mValues;
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ // OK to print UserData because UserData.toString() is PII-aware
+ final StringBuilder builder = new StringBuilder("genericUserData=")
+ .append(mGenericUserData)
+ .append(", packageUserData=").append(mPackageUserData);
+ return builder.toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mGenericUserData, 0);
+ parcel.writeParcelable(mPackageUserData, 0);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<CompositeUserData> CREATOR =
+ new Parcelable.Creator<CompositeUserData>() {
+ @Override
+ public CompositeUserData createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final UserData genericUserData = parcel.readParcelable(null);
+ final UserData packageUserData = parcel.readParcelable(null);
+ return new CompositeUserData(genericUserData, packageUserData);
+ }
+
+ @Override
+ public CompositeUserData[] newArray(int size) {
+ return new CompositeUserData[size];
+ }
+ };
+}
diff --git a/android/service/autofill/CustomDescription.java b/android/service/autofill/CustomDescription.java
new file mode 100644
index 0000000..e274460
--- /dev/null
+++ b/android/service/autofill/CustomDescription.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+
+/**
+ * Defines a custom description for the autofill save UI.
+ *
+ * <p>This is useful when the autofill service needs to show a detailed view of what would be saved;
+ * for example, when the screen contains a credit card, it could display a logo of the credit card
+ * bank, the last four digits of the credit card number, and its expiration number.
+ *
+ * <p>A custom description is made of 2 parts:
+ * <ul>
+ * <li>A {@link RemoteViews presentation template} containing children views.
+ * <li>{@link Transformation Transformations} to populate the children views.
+ * </ul>
+ *
+ * <p>For the credit card example mentioned above, the (simplified) template would be:
+ *
+ * <pre class="prettyprint">
+ * <LinearLayout>
+ * <ImageView android:id="@+id/templateccLogo"/>
+ * <TextView android:id="@+id/templateCcNumber"/>
+ * <TextView android:id="@+id/templateExpDate"/>
+ * </LinearLayout>
+ * </pre>
+ *
+ * <p>Which in code translates to:
+ *
+ * <pre class="prettyprint">
+ * CustomDescription.Builder buider = new Builder(new RemoteViews(pgkName, R.layout.cc_template);
+ * </pre>
+ *
+ * <p>Then the value of each of the 3 children would be changed at runtime based on the the value of
+ * the screen fields and the {@link Transformation Transformations}:
+ *
+ * <pre class="prettyprint">
+ * // Image child - different logo for each bank, based on credit card prefix
+ * builder.addChild(R.id.templateccLogo,
+ * new ImageTransformation.Builder(ccNumberId)
+ * .addOption(Pattern.compile("^4815.*$"), R.drawable.ic_credit_card_logo1)
+ * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2)
+ * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3)
+ * .build();
+ * // Masked credit card number (as .....LAST_4_DIGITS)
+ * builder.addChild(R.id.templateCcNumber, new CharSequenceTransformation
+ * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
+ * .build();
+ * // Expiration date as MM / YYYY:
+ * builder.addChild(R.id.templateExpDate, new CharSequenceTransformation
+ * .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
+ * .addField(ccExpYearId, Pattern.compile("^(\\d\\d)$"), "/$1")
+ * .build();
+ * </pre>
+ *
+ * <p>See {@link ImageTransformation}, {@link CharSequenceTransformation} for more info about these
+ * transformations.
+ */
+public final class CustomDescription implements Parcelable {
+
+ private final RemoteViews mPresentation;
+ private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
+ private final ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates;
+ private final SparseArray<InternalOnClickAction> mActions;
+
+ private CustomDescription(Builder builder) {
+ mPresentation = builder.mPresentation;
+ mTransformations = builder.mTransformations;
+ mUpdates = builder.mUpdates;
+ mActions = builder.mActions;
+ }
+
+ /** @hide */
+ @Nullable
+ public RemoteViews getPresentation() {
+ return mPresentation;
+ }
+
+ /** @hide */
+ @Nullable
+ public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() {
+ return mTransformations;
+ }
+
+ /** @hide */
+ @Nullable
+ public ArrayList<Pair<InternalValidator, BatchUpdates>> getUpdates() {
+ return mUpdates;
+ }
+
+ /** @hide */
+ @Nullable
+ @TestApi
+ public SparseArray<InternalOnClickAction> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Builder for {@link CustomDescription} objects.
+ */
+ public static class Builder {
+ private final RemoteViews mPresentation;
+
+ private boolean mDestroyed;
+ private ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
+ private ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates;
+ private SparseArray<InternalOnClickAction> mActions;
+
+ /**
+ * Default constructor.
+ *
+ * <p><b>Note:</b> If any child view of presentation triggers a
+ * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent) pending intent
+ * on click}, such {@link PendingIntent} must follow the restrictions below, otherwise
+ * it might not be triggered or the autofill save UI might not be shown when its activity
+ * is finished:
+ * <ul>
+ * <li>It cannot be created with the {@link PendingIntent#FLAG_IMMUTABLE} flag.
+ * <li>It must be a PendingIntent for an {@link Activity}.
+ * <li>The activity must call {@link Activity#finish()} when done.
+ * <li>The activity should not launch other activities.
+ * </ul>
+ *
+ * @param parentPresentation template presentation with (optional) children views.
+ * @throws NullPointerException if {@code parentPresentation} is null (on Android
+ * {@link android.os.Build.VERSION_CODES#P} or higher).
+ */
+ public Builder(@NonNull RemoteViews parentPresentation) {
+ mPresentation = Preconditions.checkNotNull(parentPresentation);
+ }
+
+ /**
+ * Adds a transformation to replace the value of a child view with the fields in the
+ * screen.
+ *
+ * <p>When multiple transformations are added for the same child view, they will be applied
+ * in the same order as added.
+ *
+ * @param id view id of the children view.
+ * @param transformation an implementation provided by the Android System.
+ *
+ * @return this builder.
+ *
+ * @throws IllegalArgumentException if {@code transformation} is not a class provided
+ * by the Android System.
+ * @throws IllegalStateException if {@link #build()} was already called.
+ */
+ @NonNull
+ public Builder addChild(int id, @NonNull Transformation transformation) {
+ throwIfDestroyed();
+ Preconditions.checkArgument((transformation instanceof InternalTransformation),
+ "not provided by Android System: " + transformation);
+ if (mTransformations == null) {
+ mTransformations = new ArrayList<>();
+ }
+ mTransformations.add(new Pair<>(id, (InternalTransformation) transformation));
+ return this;
+ }
+
+ /**
+ * Updates the {@link RemoteViews presentation template} when a condition is satisfied by
+ * applying a series of remote view operations. This allows dynamic customization of the
+ * portion of the save UI that is controlled by the autofill service. Such dynamic
+ * customization is based on the content of target views.
+ *
+ * <p>The updates are applied in the sequence they are added, after the
+ * {@link #addChild(int, Transformation) transformations} are applied to the children
+ * views.
+ *
+ * <p>For example, to make children views visible when fields are not empty:
+ *
+ * <pre class="prettyprint">
+ * RemoteViews template = new RemoteViews(pgkName, R.layout.my_full_template);
+ *
+ * Pattern notEmptyPattern = Pattern.compile(".+");
+ * Validator hasAddress = new RegexValidator(addressAutofillId, notEmptyPattern);
+ * Validator hasCcNumber = new RegexValidator(ccNumberAutofillId, notEmptyPattern);
+ *
+ * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_full_template)
+ * addressUpdates.setViewVisibility(R.id.address, View.VISIBLE);
+ *
+ * // Make address visible
+ * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder()
+ * .updateTemplate(addressUpdates)
+ * .build();
+ *
+ * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_full_template)
+ * ccUpdates.setViewVisibility(R.id.cc_number, View.VISIBLE);
+ *
+ * // Mask credit card number (as .....LAST_4_DIGITS) and make it visible
+ * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder()
+ * .updateTemplate(ccUpdates)
+ * .transformChild(R.id.templateCcNumber, new CharSequenceTransformation
+ * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
+ * .build())
+ * .build();
+ *
+ * CustomDescription customDescription = new CustomDescription.Builder(template)
+ * .batchUpdate(hasAddress, addressBatchUpdates)
+ * .batchUpdate(hasCcNumber, ccBatchUpdates)
+ * .build();
+ * </pre>
+ *
+ * <p>Another approach is to add a child first, then apply the transformations. Example:
+ *
+ * <pre class="prettyprint">
+ * RemoteViews template = new RemoteViews(pgkName, R.layout.my_base_template);
+ *
+ * RemoteViews addressPresentation = new RemoteViews(pgkName, R.layout.address)
+ * RemoteViews addressUpdates = new RemoteViews(pgkName, R.layout.my_template)
+ * addressUpdates.addView(R.id.parentId, addressPresentation);
+ * BatchUpdates addressBatchUpdates = new BatchUpdates.Builder()
+ * .updateTemplate(addressUpdates)
+ * .build();
+ *
+ * RemoteViews ccPresentation = new RemoteViews(pgkName, R.layout.cc)
+ * RemoteViews ccUpdates = new RemoteViews(pgkName, R.layout.my_template)
+ * ccUpdates.addView(R.id.parentId, ccPresentation);
+ * BatchUpdates ccBatchUpdates = new BatchUpdates.Builder()
+ * .updateTemplate(ccUpdates)
+ * .transformChild(R.id.templateCcNumber, new CharSequenceTransformation
+ * .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
+ * .build())
+ * .build();
+ *
+ * CustomDescription customDescription = new CustomDescription.Builder(template)
+ * .batchUpdate(hasAddress, addressBatchUpdates)
+ * .batchUpdate(hasCcNumber, ccBatchUpdates)
+ * .build();
+ * </pre>
+ *
+ * @param condition condition used to trigger the updates.
+ * @param updates actions to be applied to the
+ * {@link #Builder(RemoteViews) template presentation} when the condition
+ * is satisfied.
+ *
+ * @return this builder
+ *
+ * @throws IllegalArgumentException if {@code condition} is not a class provided
+ * by the Android System.
+ * @throws IllegalStateException if {@link #build()} was already called.
+ */
+ @NonNull
+ public Builder batchUpdate(@NonNull Validator condition, @NonNull BatchUpdates updates) {
+ throwIfDestroyed();
+ Preconditions.checkArgument((condition instanceof InternalValidator),
+ "not provided by Android System: " + condition);
+ Preconditions.checkNotNull(updates);
+ if (mUpdates == null) {
+ mUpdates = new ArrayList<>();
+ }
+ mUpdates.add(new Pair<>((InternalValidator) condition, updates));
+ return this;
+ }
+
+ /**
+ * Sets an action to be applied to the {@link RemoteViews presentation template} when the
+ * child view with the given {@code id} is clicked.
+ *
+ * <p>Typically used when the presentation uses a masked field (like {@code ****}) for
+ * sensitive fields like passwords or credit cards numbers, but offers a an icon that the
+ * user can tap to show the value for that field.
+ *
+ * <p>Example:
+ *
+ * <pre class="prettyprint">
+ * customDescriptionBuilder
+ * .addChild(R.id.password_plain, new CharSequenceTransformation
+ * .Builder(passwordId, Pattern.compile("^(.*)$"), "$1").build())
+ * .addOnClickAction(R.id.showIcon, new VisibilitySetterAction
+ * .Builder(R.id.hideIcon, View.VISIBLE)
+ * .setVisibility(R.id.showIcon, View.GONE)
+ * .setVisibility(R.id.password_plain, View.VISIBLE)
+ * .setVisibility(R.id.password_masked, View.GONE)
+ * .build())
+ * .addOnClickAction(R.id.hideIcon, new VisibilitySetterAction
+ * .Builder(R.id.showIcon, View.VISIBLE)
+ * .setVisibility(R.id.hideIcon, View.GONE)
+ * .setVisibility(R.id.password_masked, View.VISIBLE)
+ * .setVisibility(R.id.password_plain, View.GONE)
+ * .build());
+ * </pre>
+ *
+ * <p><b>Note:</b> Currently only one action can be applied to a child; if this method
+ * is called multiple times passing the same {@code id}, only the last call will be used.
+ *
+ * @param id resource id of the child view.
+ * @param action action to be performed. Must be an an implementation provided by the
+ * Android System.
+ *
+ * @return this builder
+ *
+ * @throws IllegalArgumentException if {@code action} is not a class provided
+ * by the Android System.
+ * @throws IllegalStateException if {@link #build()} was already called.
+ */
+ @NonNull
+ public Builder addOnClickAction(int id, @NonNull OnClickAction action) {
+ throwIfDestroyed();
+ Preconditions.checkArgument((action instanceof InternalOnClickAction),
+ "not provided by Android System: " + action);
+ if (mActions == null) {
+ mActions = new SparseArray<InternalOnClickAction>();
+ }
+ mActions.put(id, (InternalOnClickAction) action);
+
+ return this;
+ }
+
+ /**
+ * Creates a new {@link CustomDescription} instance.
+ */
+ @NonNull
+ public CustomDescription build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new CustomDescription(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return new StringBuilder("CustomDescription: [presentation=")
+ .append(mPresentation)
+ .append(", transformations=")
+ .append(mTransformations == null ? "N/A" : mTransformations.size())
+ .append(", updates=")
+ .append(mUpdates == null ? "N/A" : mUpdates.size())
+ .append(", actions=")
+ .append(mActions == null ? "N/A" : mActions.size())
+ .append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mPresentation, flags);
+ if (mPresentation == null) return;
+
+ if (mTransformations == null) {
+ dest.writeIntArray(null);
+ } else {
+ final int size = mTransformations.size();
+ final int[] ids = new int[size];
+ final InternalTransformation[] values = new InternalTransformation[size];
+ for (int i = 0; i < size; i++) {
+ final Pair<Integer, InternalTransformation> pair = mTransformations.get(i);
+ ids[i] = pair.first;
+ values[i] = pair.second;
+ }
+ dest.writeIntArray(ids);
+ dest.writeParcelableArray(values, flags);
+ }
+ if (mUpdates == null) {
+ dest.writeParcelableArray(null, flags);
+ } else {
+ final int size = mUpdates.size();
+ final InternalValidator[] conditions = new InternalValidator[size];
+ final BatchUpdates[] updates = new BatchUpdates[size];
+
+ for (int i = 0; i < size; i++) {
+ final Pair<InternalValidator, BatchUpdates> pair = mUpdates.get(i);
+ conditions[i] = pair.first;
+ updates[i] = pair.second;
+ }
+ dest.writeParcelableArray(conditions, flags);
+ dest.writeParcelableArray(updates, flags);
+ }
+ if (mActions == null) {
+ dest.writeIntArray(null);
+ } else {
+ final int size = mActions.size();
+ final int[] ids = new int[size];
+ final InternalOnClickAction[] values = new InternalOnClickAction[size];
+ for (int i = 0; i < size; i++) {
+ ids[i] = mActions.keyAt(i);
+ values[i] = mActions.valueAt(i);
+ }
+ dest.writeIntArray(ids);
+ dest.writeParcelableArray(values, flags);
+ }
+ }
+ public static final @android.annotation.NonNull Parcelable.Creator<CustomDescription> CREATOR =
+ new Parcelable.Creator<CustomDescription>() {
+ @Override
+ public CustomDescription createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final RemoteViews parentPresentation = parcel.readParcelable(null);
+ if (parentPresentation == null) return null;
+
+ final Builder builder = new Builder(parentPresentation);
+ final int[] transformationIds = parcel.createIntArray();
+ if (transformationIds != null) {
+ final InternalTransformation[] values =
+ parcel.readParcelableArray(null, InternalTransformation.class);
+ final int size = transformationIds.length;
+ for (int i = 0; i < size; i++) {
+ builder.addChild(transformationIds[i], values[i]);
+ }
+ }
+ final InternalValidator[] conditions =
+ parcel.readParcelableArray(null, InternalValidator.class);
+ if (conditions != null) {
+ final BatchUpdates[] updates = parcel.readParcelableArray(null, BatchUpdates.class);
+ final int size = conditions.length;
+ for (int i = 0; i < size; i++) {
+ builder.batchUpdate(conditions[i], updates[i]);
+ }
+ }
+ final int[] actionIds = parcel.createIntArray();
+ if (actionIds != null) {
+ final InternalOnClickAction[] values =
+ parcel.readParcelableArray(null, InternalOnClickAction.class);
+ final int size = actionIds.length;
+ for (int i = 0; i < size; i++) {
+ builder.addOnClickAction(actionIds[i], values[i]);
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public CustomDescription[] newArray(int size) {
+ return new CustomDescription[size];
+ }
+ };
+}
diff --git a/android/service/autofill/Dataset.java b/android/service/autofill/Dataset.java
new file mode 100644
index 0000000..08aa534
--- /dev/null
+++ b/android/service/autofill/Dataset.java
@@ -0,0 +1,789 @@
+/*
+ * Copyright (C) 2016 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.IntentSender;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * <p>A <code>Dataset</code> object represents a group of fields (key / value pairs) used
+ * to autofill parts of a screen.
+ *
+ * <p>For more information about the role of datasets in the autofill workflow, read
+ * <a href="/guide/topics/text/autofill-services">Build autofill services</a> and the
+ * <code><a href="/reference/android/service/autofill/AutofillService">AutofillService</a></code>
+ * documentation.
+ *
+ * <a name="BasicUsage"></a>
+ * <h3>Basic usage</h3>
+ *
+ * <p>In its simplest form, a dataset contains one or more fields (comprised of
+ * an {@link AutofillId id}, a {@link AutofillValue value}, and an optional filter
+ * {@link Pattern regex}); and one or more {@link RemoteViews presentations} for these fields
+ * (each field could have its own {@link RemoteViews presentation}, or use the default
+ * {@link RemoteViews presentation} associated with the whole dataset).
+ *
+ * <p>When an autofill service returns datasets in a {@link FillResponse}
+ * and the screen input is focused in a view that is present in at least one of these datasets,
+ * the Android System displays a UI containing the {@link RemoteViews presentation} of
+ * all datasets pairs that have that view's {@link AutofillId}. Then, when the user selects a
+ * dataset from the UI, all views in that dataset are autofilled.
+ *
+ * <p>If both the current Input Method and autofill service supports inline suggestions, the Dataset
+ * can be shown by the keyboard as a suggestion. To use this feature, the Dataset should contain
+ * an {@link InlinePresentation} representing how the inline suggestion UI will be rendered.
+ *
+ * <a name="Authentication"></a>
+ * <h3>Dataset authentication</h3>
+ *
+ * <p>In a more sophisticated form, the dataset values can be protected until the user authenticates
+ * the dataset—in that case, when a dataset is selected by the user, the Android System
+ * launches an intent set by the service to "unlock" the dataset.
+ *
+ * <p>For example, when a data set contains credit card information (such as number,
+ * expiration date, and verification code), you could provide a dataset presentation saying
+ * "Tap to authenticate". Then when the user taps that option, you would launch an activity asking
+ * the user to enter the credit card code, and if the user enters a valid code, you could then
+ * "unlock" the dataset.
+ *
+ * <p>You can also use authenticated datasets to offer an interactive UI for the user. For example,
+ * if the activity being autofilled is an account creation screen, you could use an authenticated
+ * dataset to automatically generate a random password for the user.
+ *
+ * <p>See {@link Dataset.Builder#setAuthentication(IntentSender)} for more details about the dataset
+ * authentication mechanism.
+ *
+ * <a name="Filtering"></a>
+ * <h3>Filtering</h3>
+ * <p>The autofill UI automatically changes which values are shown based on value of the view
+ * anchoring it, following the rules below:
+ * <ol>
+ * <li>If the view's {@link android.view.View#getAutofillValue() autofill value} is not
+ * {@link AutofillValue#isText() text} or is empty, all datasets are shown.
+ * <li>Datasets that have a filter regex (set through
+ * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern)} or
+ * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}) and whose
+ * regex matches the view's text value converted to lower case are shown.
+ * <li>Datasets that do not require authentication, have a field value that is
+ * {@link AutofillValue#isText() text} and whose {@link AutofillValue#getTextValue() value} starts
+ * with the lower case value of the view's text are shown.
+ * <li>All other datasets are hidden.
+ * </ol>
+ *
+ */
+public final class Dataset implements Parcelable {
+
+ private final ArrayList<AutofillId> mFieldIds;
+ private final ArrayList<AutofillValue> mFieldValues;
+ private final ArrayList<RemoteViews> mFieldPresentations;
+ private final ArrayList<InlinePresentation> mFieldInlinePresentations;
+ private final ArrayList<DatasetFieldFilter> mFieldFilters;
+ private final RemoteViews mPresentation;
+ @Nullable private final InlinePresentation mInlinePresentation;
+ private final IntentSender mAuthentication;
+ @Nullable String mId;
+
+ private Dataset(Builder builder) {
+ mFieldIds = builder.mFieldIds;
+ mFieldValues = builder.mFieldValues;
+ mFieldPresentations = builder.mFieldPresentations;
+ mFieldInlinePresentations = builder.mFieldInlinePresentations;
+ mFieldFilters = builder.mFieldFilters;
+ mPresentation = builder.mPresentation;
+ mInlinePresentation = builder.mInlinePresentation;
+ mAuthentication = builder.mAuthentication;
+ mId = builder.mId;
+ }
+
+ /** @hide */
+ public @Nullable ArrayList<AutofillId> getFieldIds() {
+ return mFieldIds;
+ }
+
+ /** @hide */
+ public @Nullable ArrayList<AutofillValue> getFieldValues() {
+ return mFieldValues;
+ }
+
+ /** @hide */
+ public RemoteViews getFieldPresentation(int index) {
+ final RemoteViews customPresentation = mFieldPresentations.get(index);
+ return customPresentation != null ? customPresentation : mPresentation;
+ }
+
+ /** @hide */
+ @Nullable
+ public InlinePresentation getFieldInlinePresentation(int index) {
+ final InlinePresentation inlinePresentation = mFieldInlinePresentations.get(index);
+ return inlinePresentation != null ? inlinePresentation : mInlinePresentation;
+ }
+
+ /** @hide */
+ @Nullable
+ public DatasetFieldFilter getFilter(int index) {
+ return mFieldFilters.get(index);
+ }
+
+ /** @hide */
+ public @Nullable IntentSender getAuthentication() {
+ return mAuthentication;
+ }
+
+ /** @hide */
+ public boolean isEmpty() {
+ return mFieldIds == null || mFieldIds.isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ final StringBuilder builder = new StringBuilder("Dataset[");
+ if (mId == null) {
+ builder.append("noId");
+ } else {
+ // Cannot disclose id because it could contain PII.
+ builder.append("id=").append(mId.length()).append("_chars");
+ }
+ if (mFieldIds != null) {
+ builder.append(", fieldIds=").append(mFieldIds);
+ }
+ if (mFieldValues != null) {
+ builder.append(", fieldValues=").append(mFieldValues);
+ }
+ if (mFieldPresentations != null) {
+ builder.append(", fieldPresentations=").append(mFieldPresentations.size());
+ }
+ if (mFieldInlinePresentations != null) {
+ builder.append(", fieldInlinePresentations=").append(mFieldInlinePresentations.size());
+ }
+ if (mFieldFilters != null) {
+ builder.append(", fieldFilters=").append(mFieldFilters.size());
+ }
+ if (mPresentation != null) {
+ builder.append(", hasPresentation");
+ }
+ if (mInlinePresentation != null) {
+ builder.append(", hasInlinePresentation");
+ }
+ if (mAuthentication != null) {
+ builder.append(", hasAuthentication");
+ }
+ return builder.append(']').toString();
+ }
+
+ /**
+ * Gets the id of this dataset.
+ *
+ * @return The id of this dataset or {@code null} if not set
+ *
+ * @hide
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * A builder for {@link Dataset} objects. You must provide at least
+ * one value for a field or set an authentication intent.
+ */
+ public static final class Builder {
+ private ArrayList<AutofillId> mFieldIds;
+ private ArrayList<AutofillValue> mFieldValues;
+ private ArrayList<RemoteViews> mFieldPresentations;
+ private ArrayList<InlinePresentation> mFieldInlinePresentations;
+ private ArrayList<DatasetFieldFilter> mFieldFilters;
+ private RemoteViews mPresentation;
+ @Nullable private InlinePresentation mInlinePresentation;
+ private IntentSender mAuthentication;
+ private boolean mDestroyed;
+ @Nullable private String mId;
+
+ /**
+ * Creates a new builder.
+ *
+ * @param presentation The presentation used to visualize this dataset.
+ */
+ public Builder(@NonNull RemoteViews presentation) {
+ Preconditions.checkNotNull(presentation, "presentation must be non-null");
+ mPresentation = presentation;
+ }
+
+ /**
+ * Creates a new builder.
+ *
+ * <p>Only called by augmented autofill.
+ *
+ * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset
+ * as inline suggestions. If the dataset supports inline suggestions,
+ * this should not be null.
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public Builder(@NonNull InlinePresentation inlinePresentation) {
+ Preconditions.checkNotNull(inlinePresentation, "inlinePresentation must be non-null");
+ mInlinePresentation = inlinePresentation;
+ }
+
+ /**
+ * Creates a new builder for a dataset where each field will be visualized independently.
+ *
+ * <p>When using this constructor, fields must be set through
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} or
+ * {@link #setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Sets the {@link InlinePresentation} used to visualize this dataset as inline suggestions.
+ * If the dataset supports inline suggestions this should not be null.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setInlinePresentation(
+ @NonNull InlinePresentation inlinePresentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(inlinePresentation, "inlinePresentation must be non-null");
+ mInlinePresentation = inlinePresentation;
+ return this;
+ }
+
+ /**
+ * Triggers a custom UI before before autofilling the screen with the contents of this
+ * dataset.
+ *
+ * <p><b>Note:</b> Although the name of this method suggests that it should be used just for
+ * authentication flow, it can be used for other advanced flows; see {@link AutofillService}
+ * for examples.
+ *
+ * <p>This method is called when you need to provide an authentication
+ * UI for the data set. For example, when a data set contains credit card information
+ * (such as number, expiration date, and verification code), you can display UI
+ * asking for the verification code before filing in the data. Even if the
+ * data set is completely populated the system will launch the specified authentication
+ * intent and will need your approval to fill it in. Since the data set is "locked"
+ * until the user authenticates it, typically this data set name is masked
+ * (for example, "VISA....1234"). Typically you would want to store the data set
+ * labels non-encrypted and the actual sensitive data encrypted and not in memory.
+ * This allows showing the labels in the UI while involving the user if one of
+ * the items with these labels is chosen. Note that if you use sensitive data as
+ * a label, for example an email address, then it should also be encrypted.</p>
+ *
+ * <p>When a user triggers autofill, the system launches the provided intent
+ * whose extras will have the {@link
+ * android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen content},
+ * and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE client
+ * state}. Once you complete your authentication flow you should set the activity
+ * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated
+ * {@link Dataset dataset} or a fully-populated {@link FillResponse response} by
+ * setting it to the {@link
+ * android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra. If you
+ * provide a dataset in the result, it will replace the authenticated dataset and
+ * will be immediately filled in. If you provide a response, it will replace the
+ * current response and the UI will be refreshed. For example, if you provided
+ * credit card information without the CVV for the data set in the {@link FillResponse
+ * response} then the returned data set should contain the CVV entry.
+ *
+ * <p><b>Note:</b> Do not make the provided pending intent
+ * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
+ * platform needs to fill in the authentication arguments.
+ *
+ * @param authentication Intent to an activity with your authentication flow.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ *
+ * @see android.app.PendingIntent
+ */
+ public @NonNull Builder setAuthentication(@Nullable IntentSender authentication) {
+ throwIfDestroyed();
+ mAuthentication = authentication;
+ return this;
+ }
+
+ /**
+ * Sets the id for the dataset so its usage can be tracked.
+ *
+ * <p>Dataset usage can be tracked for 2 purposes:
+ *
+ * <ul>
+ * <li>For statistical purposes, the service can call
+ * {@link AutofillService#getFillEventHistory()} when handling {@link
+ * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}
+ * calls.
+ * <li>For normal autofill workflow, the service can call
+ * {@link SaveRequest#getDatasetIds()} when handling
+ * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} calls.
+ * </ul>
+ *
+ * @param id id for this dataset or {@code null} to unset.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setId(@Nullable String id) {
+ throwIfDestroyed();
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Sets the value of a field.
+ *
+ * <b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, this method would
+ * throw an {@link IllegalStateException} if this builder was constructed without a
+ * {@link RemoteViews presentation}. Android {@link android.os.Build.VERSION_CODES#P} and
+ * higher removed this restriction because datasets used as an
+ * {@link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT
+ * authentication result} do not need a presentation. But if you don't set the presentation
+ * in the constructor in a dataset that is meant to be shown to the user, the autofill UI
+ * for this field will not be displayed.
+ *
+ * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and
+ * higher, datasets that require authentication can be also be filtered by passing a
+ * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) {
+ throwIfDestroyed();
+ setLifeTheUniverseAndEverything(id, value, null, null, null);
+ return this;
+ }
+
+ /**
+ * Sets the value of a field, using a custom {@link RemoteViews presentation} to
+ * visualize it.
+ *
+ * <p><b>Note:</b> On Android {@link android.os.Build.VERSION_CODES#P} and
+ * higher, datasets that require authentication can be also be filtered by passing a
+ * {@link AutofillValue#forText(CharSequence) text value} as the {@code value} parameter.
+ *
+ * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
+ * or background color: Autofill on different platforms may have different themes.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param presentation the presentation used to visualize this field.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
+ @NonNull RemoteViews presentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(presentation, "presentation cannot be null");
+ setLifeTheUniverseAndEverything(id, value, presentation, null, null);
+ return this;
+ }
+
+ /**
+ * Sets the value of a field using an <a href="#Filtering">explicit filter</a>.
+ *
+ * <p>This method is typically used when the dataset requires authentication and the service
+ * does not know its value but wants to hide the dataset after the user enters a minimum
+ * number of characters. For example, if the dataset represents a credit card number and the
+ * service does not want to show the "Tap to authenticate" message until the user tapped
+ * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}.
+ *
+ * <p><b>Note:</b> If the dataset requires authentication but the service knows its text
+ * value it's easier to filter by calling {@link #setValue(AutofillId, AutofillValue)} and
+ * use the value to filter.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param filter regex used to determine if the dataset should be shown in the autofill UI;
+ * when {@code null}, it disables filtering on that dataset (this is the recommended
+ * approach when {@code value} is not {@code null} and field contains sensitive data
+ * such as passwords).
+ *
+ * @return this builder.
+ * @throws IllegalStateException if the builder was constructed without a
+ * {@link RemoteViews presentation} or {@link #build()} was already called.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
+ @Nullable Pattern filter) {
+ throwIfDestroyed();
+ Preconditions.checkState(mPresentation != null,
+ "Dataset presentation not set on constructor");
+ setLifeTheUniverseAndEverything(id, value, null, null, new DatasetFieldFilter(filter));
+ return this;
+ }
+
+ /**
+ * Sets the value of a field, using a custom {@link RemoteViews presentation} to
+ * visualize it and a <a href="#Filtering">explicit filter</a>.
+ *
+ * <p>This method is typically used when the dataset requires authentication and the service
+ * does not know its value but wants to hide the dataset after the user enters a minimum
+ * number of characters. For example, if the dataset represents a credit card number and the
+ * service does not want to show the "Tap to authenticate" message until the user tapped
+ * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}.
+ *
+ * <p><b>Note:</b> If the dataset requires authentication but the service knows its text
+ * value it's easier to filter by calling
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param filter regex used to determine if the dataset should be shown in the autofill UI;
+ * when {@code null}, it disables filtering on that dataset (this is the recommended
+ * approach when {@code value} is not {@code null} and field contains sensitive data
+ * such as passwords).
+ * @param presentation the presentation used to visualize this field.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
+ @Nullable Pattern filter, @NonNull RemoteViews presentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(presentation, "presentation cannot be null");
+ setLifeTheUniverseAndEverything(id, value, presentation, null,
+ new DatasetFieldFilter(filter));
+ return this;
+ }
+
+ /**
+ * Sets the value of a field, using a custom {@link RemoteViews presentation} to
+ * visualize it and an {@link InlinePresentation} to visualize it as an inline suggestion.
+ *
+ * <p><b>Note:</b> If the dataset requires authentication but the service knows its text
+ * value it's easier to filter by calling
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param presentation the presentation used to visualize this field.
+ * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset
+ * as inline suggestions. If the dataset supports inline suggestions,
+ * this should not be null.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
+ @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(presentation, "presentation cannot be null");
+ Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null");
+ setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, null);
+ return this;
+ }
+
+ /**
+ * Sets the value of a field, using a custom {@link RemoteViews presentation} to
+ * visualize it and a <a href="#Filtering">explicit filter</a>, and an
+ * {@link InlinePresentation} to visualize it as an inline suggestion.
+ *
+ * <p>This method is typically used when the dataset requires authentication and the service
+ * does not know its value but wants to hide the dataset after the user enters a minimum
+ * number of characters. For example, if the dataset represents a credit card number and the
+ * service does not want to show the "Tap to authenticate" message until the user tapped
+ * 4 digits, in which case the filter would be {@code Pattern.compile("\\d.{4,}")}.
+ *
+ * <p><b>Note:</b> If the dataset requires authentication but the service knows its text
+ * value it's easier to filter by calling
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} and using the value to filter.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param filter regex used to determine if the dataset should be shown in the autofill UI;
+ * when {@code null}, it disables filtering on that dataset (this is the recommended
+ * approach when {@code value} is not {@code null} and field contains sensitive data
+ * such as passwords).
+ * @param presentation the presentation used to visualize this field.
+ * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset
+ * as inline suggestions. If the dataset supports inline suggestions, this
+ * should not be null.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ */
+ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
+ @Nullable Pattern filter, @NonNull RemoteViews presentation,
+ @NonNull InlinePresentation inlinePresentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(presentation, "presentation cannot be null");
+ Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null");
+ setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation,
+ new DatasetFieldFilter(filter));
+ return this;
+ }
+
+ /**
+ * Sets the value of a field with an <a href="#Filtering">explicit filter</a>, and using an
+ * {@link InlinePresentation} to visualize it as an inline suggestion.
+ *
+ * <p>Only called by augmented autofill.
+ *
+ * @param id id returned by {@link
+ * android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
+ * @param value the value to be autofilled. Pass {@code null} if you do not have the value
+ * but the target view is a logical part of the dataset. For example, if
+ * the dataset needs authentication and you have no access to the value.
+ * @param filter regex used to determine if the dataset should be shown in the autofill UI;
+ * when {@code null}, it disables filtering on that dataset (this is the recommended
+ * approach when {@code value} is not {@code null} and field contains sensitive data
+ * such as passwords).
+ * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset
+ * as inline suggestions. If the dataset supports inline suggestions, this
+ * should not be null.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return this builder.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public @NonNull Builder setFieldInlinePresentation(@NonNull AutofillId id,
+ @Nullable AutofillValue value, @Nullable Pattern filter,
+ @NonNull InlinePresentation inlinePresentation) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null");
+ setLifeTheUniverseAndEverything(id, value, null, inlinePresentation,
+ new DatasetFieldFilter(filter));
+ return this;
+ }
+
+ private void setLifeTheUniverseAndEverything(@NonNull AutofillId id,
+ @Nullable AutofillValue value, @Nullable RemoteViews presentation,
+ @Nullable InlinePresentation inlinePresentation,
+ @Nullable DatasetFieldFilter filter) {
+ Preconditions.checkNotNull(id, "id cannot be null");
+ if (mFieldIds != null) {
+ final int existingIdx = mFieldIds.indexOf(id);
+ if (existingIdx >= 0) {
+ mFieldValues.set(existingIdx, value);
+ mFieldPresentations.set(existingIdx, presentation);
+ mFieldInlinePresentations.set(existingIdx, inlinePresentation);
+ mFieldFilters.set(existingIdx, filter);
+ return;
+ }
+ } else {
+ mFieldIds = new ArrayList<>();
+ mFieldValues = new ArrayList<>();
+ mFieldPresentations = new ArrayList<>();
+ mFieldInlinePresentations = new ArrayList<>();
+ mFieldFilters = new ArrayList<>();
+ }
+ mFieldIds.add(id);
+ mFieldValues.add(value);
+ mFieldPresentations.add(presentation);
+ mFieldInlinePresentations.add(inlinePresentation);
+ mFieldFilters.add(filter);
+ }
+
+ /**
+ * Creates a new {@link Dataset} instance.
+ *
+ * <p>You should not interact with this builder once this method is called.
+ *
+ * @throws IllegalStateException if no field was set (through
+ * {@link #setValue(AutofillId, AutofillValue)} or
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews)} or
+ * {@link #setValue(AutofillId, AutofillValue, RemoteViews, InlinePresentation)}),
+ * or if {@link #build()} was already called.
+ *
+ * @return The built dataset.
+ */
+ public @NonNull Dataset build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ if (mFieldIds == null) {
+ throw new IllegalStateException("at least one value must be set");
+ }
+ return new Dataset(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mPresentation, flags);
+ parcel.writeParcelable(mInlinePresentation, flags);
+ parcel.writeTypedList(mFieldIds, flags);
+ parcel.writeTypedList(mFieldValues, flags);
+ parcel.writeTypedList(mFieldPresentations, flags);
+ parcel.writeTypedList(mFieldInlinePresentations, flags);
+ parcel.writeTypedList(mFieldFilters, flags);
+ parcel.writeParcelable(mAuthentication, flags);
+ parcel.writeString(mId);
+ }
+
+ public static final @NonNull Creator<Dataset> CREATOR = new Creator<Dataset>() {
+ @Override
+ public Dataset createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final RemoteViews presentation = parcel.readParcelable(null);
+ final InlinePresentation inlinePresentation = parcel.readParcelable(null);
+ final Builder builder = presentation != null
+ ? inlinePresentation == null
+ ? new Builder(presentation)
+ : new Builder(presentation).setInlinePresentation(inlinePresentation)
+ : inlinePresentation == null
+ ? new Builder()
+ : new Builder(inlinePresentation);
+ final ArrayList<AutofillId> ids =
+ parcel.createTypedArrayList(AutofillId.CREATOR);
+ final ArrayList<AutofillValue> values =
+ parcel.createTypedArrayList(AutofillValue.CREATOR);
+ final ArrayList<RemoteViews> presentations =
+ parcel.createTypedArrayList(RemoteViews.CREATOR);
+ final ArrayList<InlinePresentation> inlinePresentations =
+ parcel.createTypedArrayList(InlinePresentation.CREATOR);
+ final ArrayList<DatasetFieldFilter> filters =
+ parcel.createTypedArrayList(DatasetFieldFilter.CREATOR);
+ final int inlinePresentationsSize = inlinePresentations.size();
+ for (int i = 0; i < ids.size(); i++) {
+ final AutofillId id = ids.get(i);
+ final AutofillValue value = values.get(i);
+ final RemoteViews fieldPresentation = presentations.get(i);
+ final InlinePresentation fieldInlinePresentation =
+ i < inlinePresentationsSize ? inlinePresentations.get(i) : null;
+ final DatasetFieldFilter filter = filters.get(i);
+ builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation,
+ fieldInlinePresentation, filter);
+ }
+ builder.setAuthentication(parcel.readParcelable(null));
+ builder.setId(parcel.readString());
+ return builder.build();
+ }
+
+ @Override
+ public Dataset[] newArray(int size) {
+ return new Dataset[size];
+ }
+ };
+
+ /**
+ * Helper class used to indicate when the service explicitly set a {@link Pattern} filter for a
+ * dataset field‐ we cannot use a {@link Pattern} directly because then we wouldn't be
+ * able to differentiate whether the service explicitly passed a {@code null} filter to disable
+ * filter, or when it called the methods that does not take a filter {@link Pattern}.
+ *
+ * @hide
+ */
+ public static final class DatasetFieldFilter implements Parcelable {
+
+ @Nullable
+ public final Pattern pattern;
+
+ private DatasetFieldFilter(@Nullable Pattern pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ // Cannot log pattern because it could contain PII
+ return pattern == null ? "null" : pattern.pattern().length() + "_chars";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeSerializable(pattern);
+ }
+
+ @SuppressWarnings("hiding")
+ public static final @android.annotation.NonNull Creator<DatasetFieldFilter> CREATOR =
+ new Creator<DatasetFieldFilter>() {
+
+ @Override
+ public DatasetFieldFilter createFromParcel(Parcel parcel) {
+ return new DatasetFieldFilter((Pattern) parcel.readSerializable());
+ }
+
+ @Override
+ public DatasetFieldFilter[] newArray(int size) {
+ return new DatasetFieldFilter[size];
+ }
+ };
+ }
+}
diff --git a/android/service/autofill/DateTransformation.java b/android/service/autofill/DateTransformation.java
new file mode 100644
index 0000000..338ba74
--- /dev/null
+++ b/android/service/autofill/DateTransformation.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2018 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.icu.text.DateFormat;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Date;
+
+/**
+ * Replaces a {@link TextView} child of a {@link CustomDescription} with the contents of a field
+ * that is expected to have a {@link AutofillValue#forDate(long) date value}.
+ *
+ * <p>For example, a transformation to display a credit card expiration date as month/year would be:
+ *
+ * <pre class="prettyprint">
+ * new DateTransformation(ccExpDate, new java.text.SimpleDateFormat("MM/yyyy")
+ * </pre>
+ */
+public final class DateTransformation extends InternalTransformation implements
+ Transformation, Parcelable {
+ private static final String TAG = "DateTransformation";
+
+ private final AutofillId mFieldId;
+ private final DateFormat mDateFormat;
+
+ /**
+ * Creates a new transformation.
+ *
+ * @param id id of the screen field.
+ * @param dateFormat object used to transform the date value of the field to a String.
+ */
+ public DateTransformation(@NonNull AutofillId id, @NonNull DateFormat dateFormat) {
+ mFieldId = Preconditions.checkNotNull(id);
+ mDateFormat = Preconditions.checkNotNull(dateFormat);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
+ int childViewId) throws Exception {
+ final AutofillValue value = finder.findRawValueByAutofillId(mFieldId);
+ if (value == null) {
+ Log.w(TAG, "No value for id " + mFieldId);
+ return;
+ }
+ if (!value.isDate()) {
+ Log.w(TAG, "Value for " + mFieldId + " is not date: " + value);
+ return;
+ }
+
+ try {
+ final Date date = new Date(value.getDateValue());
+ final String transformed = mDateFormat.format(date);
+ if (sDebug) Log.d(TAG, "Transformed " + date + " to " + transformed);
+
+ parentTemplate.setCharSequence(childViewId, "setText", transformed);
+ } catch (Exception e) {
+ Log.w(TAG, "Could not apply " + mDateFormat + " to " + value + ": " + e);
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "DateTransformation: [id=" + mFieldId + ", format=" + mDateFormat + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mFieldId, flags);
+ parcel.writeSerializable(mDateFormat);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<DateTransformation> CREATOR =
+ new Parcelable.Creator<DateTransformation>() {
+ @Override
+ public DateTransformation createFromParcel(Parcel parcel) {
+ return new DateTransformation(parcel.readParcelable(null),
+ (DateFormat) parcel.readSerializable());
+ }
+
+ @Override
+ public DateTransformation[] newArray(int size) {
+ return new DateTransformation[size];
+ }
+ };
+}
diff --git a/android/service/autofill/DateValueSanitizer.java b/android/service/autofill/DateValueSanitizer.java
new file mode 100644
index 0000000..707bab1
--- /dev/null
+++ b/android/service/autofill/DateValueSanitizer.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.icu.text.DateFormat;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Date;
+
+/**
+ * Sanitizes a date {@link AutofillValue} using a {@link DateFormat}.
+ *
+ * <p>For example, to sanitize a credit card expiration date to just its month and year:
+ *
+ * <pre class="prettyprint">
+ * new DateValueSanitizer(new java.text.SimpleDateFormat("MM/yyyy");
+ * </pre>
+ */
+public final class DateValueSanitizer extends InternalSanitizer implements Sanitizer, Parcelable {
+
+ private static final String TAG = "DateValueSanitizer";
+
+ private final DateFormat mDateFormat;
+
+ /**
+ * Default constructor.
+ *
+ * @param dateFormat date format applied to the actual date value of an input field.
+ */
+ public DateValueSanitizer(@NonNull DateFormat dateFormat) {
+ mDateFormat = Preconditions.checkNotNull(dateFormat);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ @Nullable
+ public AutofillValue sanitize(@NonNull AutofillValue value) {
+ if (value == null) {
+ Log.w(TAG, "sanitize() called with null value");
+ return null;
+ }
+ if (!value.isDate()) {
+ if (sDebug) Log.d(TAG, value + " is not a date");
+ return null;
+ }
+
+ try {
+ final Date date = new Date(value.getDateValue());
+
+ // First convert it to string
+ final String converted = mDateFormat.format(date);
+ if (sDebug) Log.d(TAG, "Transformed " + date + " to " + converted);
+ // Then parse it back to date
+ final Date sanitized = mDateFormat.parse(converted);
+ if (sDebug) Log.d(TAG, "Sanitized to " + sanitized);
+ return AutofillValue.forDate(sanitized.getTime());
+ } catch (Exception e) {
+ Log.w(TAG, "Could not apply " + mDateFormat + " to " + value + ": " + e);
+ return null;
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "DateValueSanitizer: [dateFormat=" + mDateFormat + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeSerializable(mDateFormat);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<DateValueSanitizer> CREATOR =
+ new Parcelable.Creator<DateValueSanitizer>() {
+ @Override
+ public DateValueSanitizer createFromParcel(Parcel parcel) {
+ return new DateValueSanitizer((DateFormat) parcel.readSerializable());
+ }
+
+ @Override
+ public DateValueSanitizer[] newArray(int size) {
+ return new DateValueSanitizer[size];
+ }
+ };
+}
diff --git a/android/service/autofill/FieldClassification.java b/android/service/autofill/FieldClassification.java
new file mode 100644
index 0000000..5bf56cb
--- /dev/null
+++ b/android/service/autofill/FieldClassification.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.view.autofill.Helper;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Represents the <a href="AutofillService.html#FieldClassification">field classification</a>
+ * results for a given field.
+ */
+public final class FieldClassification {
+
+ private final ArrayList<Match> mMatches;
+
+ /** @hide */
+ public FieldClassification(@NonNull ArrayList<Match> matches) {
+ mMatches = Preconditions.checkNotNull(matches);
+ Collections.sort(mMatches, new Comparator<Match>() {
+ @Override
+ public int compare(Match o1, Match o2) {
+ if (o1.mScore > o2.mScore) return -1;
+ if (o1.mScore < o2.mScore) return 1;
+ return 0;
+ }}
+ );
+ }
+
+ /**
+ * Gets the {@link Match matches} with the highest {@link Match#getScore() scores} (sorted in
+ * descending order).
+ *
+ * <p><b>Note:</b> There's no guarantee of how many matches will be returned. In fact,
+ * the Android System might return just the top match to minimize the impact of field
+ * classification in the device's health.
+ */
+ @NonNull
+ public List<Match> getMatches() {
+ return mMatches;
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "FieldClassification: " + mMatches;
+ }
+
+ private void writeToParcel(Parcel parcel) {
+ parcel.writeInt(mMatches.size());
+ for (int i = 0; i < mMatches.size(); i++) {
+ mMatches.get(i).writeToParcel(parcel);
+ }
+ }
+
+ private static FieldClassification readFromParcel(Parcel parcel) {
+ final int size = parcel.readInt();
+ final ArrayList<Match> matches = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ matches.add(i, Match.readFromParcel(parcel));
+ }
+
+ return new FieldClassification(matches);
+ }
+
+ static FieldClassification[] readArrayFromParcel(Parcel parcel) {
+ final int length = parcel.readInt();
+ final FieldClassification[] fcs = new FieldClassification[length];
+ for (int i = 0; i < length; i++) {
+ fcs[i] = readFromParcel(parcel);
+ }
+ return fcs;
+ }
+
+ static void writeArrayToParcel(@NonNull Parcel parcel, @NonNull FieldClassification[] fcs) {
+ parcel.writeInt(fcs.length);
+ for (int i = 0; i < fcs.length; i++) {
+ fcs[i].writeToParcel(parcel);
+ }
+ }
+
+ /**
+ * Represents the score of a {@link UserData} entry for the field.
+ */
+ public static final class Match {
+
+ private final String mCategoryId;
+ private final float mScore;
+
+ /** @hide */
+ public Match(String categoryId, float score) {
+ mCategoryId = Preconditions.checkNotNull(categoryId);
+ mScore = score;
+ }
+
+ /**
+ * Gets the category id of the {@link UserData} entry.
+ */
+ @NonNull
+ public String getCategoryId() {
+ return mCategoryId;
+ }
+
+ /**
+ * Gets a classification score for the value of this field compared to the value of the
+ * {@link UserData} entry.
+ *
+ * <p>The score is based in a comparison of the field value and the user data entry, and it
+ * ranges from {@code 0.0F} to {@code 1.0F}:
+ * <ul>
+ * <li>{@code 1.0F} represents a full match ({@code 100%}).
+ * <li>{@code 0.0F} represents a full mismatch ({@code 0%}).
+ * <li>Any other value is a partial match.
+ * </ul>
+ *
+ * <p>How the score is calculated depends on the
+ * {@link UserData.Builder#setFieldClassificationAlgorithm(String, android.os.Bundle)
+ * algorithm} used.
+ */
+ public float getScore() {
+ return mScore;
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ final StringBuilder string = new StringBuilder("Match: categoryId=");
+ Helper.appendRedacted(string, mCategoryId);
+ return string.append(", score=").append(mScore).toString();
+ }
+
+ private void writeToParcel(@NonNull Parcel parcel) {
+ parcel.writeString(mCategoryId);
+ parcel.writeFloat(mScore);
+ }
+
+ private static Match readFromParcel(@NonNull Parcel parcel) {
+ return new Match(parcel.readString(), parcel.readFloat());
+ }
+ }
+}
diff --git a/android/service/autofill/FieldClassificationUserData.java b/android/service/autofill/FieldClassificationUserData.java
new file mode 100644
index 0000000..3d6cac4
--- /dev/null
+++ b/android/service/autofill/FieldClassificationUserData.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2018 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.autofill;
+
+import android.os.Bundle;
+import android.util.ArrayMap;
+
+/**
+ * Class used to define a generic UserData for field classification
+ *
+ * @hide
+ */
+public interface FieldClassificationUserData {
+ /**
+ * Gets the name of the default algorithm that is used to calculate
+ * {@link FieldClassification.Match#getScore()} match scores}.
+ */
+ String getFieldClassificationAlgorithm();
+
+ /**
+ * Gets the default field classification args.
+ */
+ Bundle getDefaultFieldClassificationArgs();
+
+ /**
+ * Gets the name of the field classification algorithm for a specific category.
+ *
+ * @param categoryId id of the specific category.
+ */
+ String getFieldClassificationAlgorithmForCategory(String categoryId);
+
+ /**
+ * Gets all field classification algorithms for specific categories.
+ */
+ ArrayMap<String, String> getFieldClassificationAlgorithms();
+
+ /**
+ * Gets all field classification args for specific categories.
+ */
+ ArrayMap<String, Bundle> getFieldClassificationArgs();
+
+ /**
+ * Gets all category ids
+ */
+ String[] getCategoryIds();
+
+ /**
+ * Gets all string values for field classification
+ */
+ String[] getValues();
+}
diff --git a/android/service/autofill/FillCallback.java b/android/service/autofill/FillCallback.java
new file mode 100644
index 0000000..06e2896
--- /dev/null
+++ b/android/service/autofill/FillCallback.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 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.autofill;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * <p><code>FillCallback</code> handles autofill requests from the {@link AutofillService} into
+ * the {@link Activity} being autofilled.
+ *
+ * <p>To learn about using Autofill services in your app, read
+ * <a href="/guide/topics/text/autofill-services">Build autofill services</a>.
+ */
+public final class FillCallback {
+
+ private static final String TAG = "FillCallback";
+
+ private final IFillCallback mCallback;
+ private final int mRequestId;
+ private boolean mCalled;
+
+ /** @hide */
+ public FillCallback(IFillCallback callback, int requestId) {
+ mCallback = callback;
+ mRequestId = requestId;
+ }
+
+ /**
+ * Notifies the Android System that a fill request
+ * ({@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
+ * FillCallback)}) was successfully fulfilled by the service.
+ *
+ * <p>This method should always be called, even if the service doesn't have the heuristics to
+ * fulfill the request (in which case it should be called with {@code null}).
+ *
+ * <p>See the main {@link AutofillService} documentation for more details and examples.
+ *
+ * @param response autofill information for that activity, or {@code null} when the service
+ * cannot autofill the activity.
+ *
+ * @throws IllegalStateException if this method or {@link #onFailure(CharSequence)} was already
+ * called.
+ */
+ public void onSuccess(@Nullable FillResponse response) {
+ assertNotCalled();
+ mCalled = true;
+
+ if (response != null) {
+ response.setRequestId(mRequestId);
+ }
+
+ try {
+ mCallback.onSuccess(response);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Notifies the Android System that a fill request (
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
+ * FillCallback)}) could not be fulfilled by the service (for example, because the user data was
+ * not available yet), so the request could be retried later.
+ *
+ * <p><b>Note: </b>this method should not be used when the service didn't have the heursitics to
+ * fulfill the request; in this case, the service should call {@link #onSuccess(FillResponse)
+ * onSuccess(null)} instead.
+ *
+ * <p><b>Note: </b>prior to {@link android.os.Build.VERSION_CODES#Q}, this
+ * method was not working as intended and the service should always call
+ * {@link #onSuccess(FillResponse) onSuccess(null)} instead.
+ *
+ * <p><b>Note: </b>for apps targeting {@link android.os.Build.VERSION_CODES#Q} or higher, this
+ * method just logs the message on {@code logcat}; for apps targetting older SDKs, it also
+ * displays the message to user using a {@link android.widget.Toast}. Generally speaking, you
+ * should not display an error to the user if the request failed, unless the request had the
+ * {@link FillRequest#FLAG_MANUAL_REQUEST} flag.
+ *
+ * @param message error message. <b>Note: </b> this message should <b>not</b> contain PII
+ * (Personally Identifiable Information, such as username or email address).
+ *
+ * @throws IllegalStateException if this method or {@link #onSuccess(FillResponse)} was already
+ * called.
+ */
+ public void onFailure(@Nullable CharSequence message) {
+ Log.w(TAG, "onFailure(): " + message);
+ assertNotCalled();
+ mCalled = true;
+ try {
+ mCallback.onFailure(mRequestId, message);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ private void assertNotCalled() {
+ if (mCalled) {
+ throw new IllegalStateException("Already called");
+ }
+ }
+}
diff --git a/android/service/autofill/FillContext.java b/android/service/autofill/FillContext.java
new file mode 100644
index 0000000..8331550
--- /dev/null
+++ b/android/service/autofill/FillContext.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.SparseIntArray;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.util.DataClass;
+
+import java.util.LinkedList;
+
+/**
+ * This class represents a context for each fill request made via {@link
+ * AutofillService#onFillRequest(FillRequest, CancellationSignal, FillCallback)}.
+ * It contains a snapshot of the UI state, the view ids that were returned by
+ * the {@link AutofillService autofill service} as both required to trigger a save
+ * and optional that can be saved, and the id of the corresponding {@link
+ * FillRequest}.
+ * <p>
+ * This context allows you to inspect the values for the interesting views
+ * in the context they appeared. Also a reference to the corresponding fill
+ * request is useful to store meta-data in the client state bundle passed
+ * to {@link FillResponse.Builder#setClientState(Bundle)} to avoid interpreting
+ * the UI state again while saving.
+ */
+@DataClass(
+ genHiddenConstructor = true,
+ genAidl = false)
+public final class FillContext implements Parcelable {
+
+ /**
+ * The id of the {@link FillRequest fill request} this context
+ * corresponds to. This is useful to associate your custom client
+ * state with every request to avoid reinterpreting the UI when saving
+ * user data.
+ */
+ private final int mRequestId;
+
+ /**
+ * The screen content.
+ */
+ private final @NonNull AssistStructure mStructure;
+
+ /**
+ * The AutofillId of the view that triggered autofill.
+ */
+ private final @NonNull AutofillId mFocusedId;
+
+ /**
+ * Lookup table AutofillId->ViewNode to speed up {@link #findViewNodesByAutofillIds}
+ * This is purely a cache and can be deleted at any time
+ */
+ private transient @Nullable ArrayMap<AutofillId, AssistStructure.ViewNode> mViewNodeLookupTable;
+
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "FillContext [reqId=" + mRequestId + ", focusedId=" + mFocusedId + "]";
+ }
+
+ /**
+ * Finds {@link ViewNode ViewNodes} that have the requested ids.
+ *
+ * @param ids The ids of the node to find.
+ *
+ * @return The nodes indexed in the same way as the ids.
+ *
+ * @hide
+ */
+ @NonNull public ViewNode[] findViewNodesByAutofillIds(@NonNull AutofillId[] ids) {
+ final LinkedList<ViewNode> nodesToProcess = new LinkedList<>();
+ final ViewNode[] foundNodes = new AssistStructure.ViewNode[ids.length];
+
+ // Indexes of foundNodes that are not found yet
+ final SparseIntArray missingNodeIndexes = new SparseIntArray(ids.length);
+
+ for (int i = 0; i < ids.length; i++) {
+ if (mViewNodeLookupTable != null) {
+ int lookupTableIndex = mViewNodeLookupTable.indexOfKey(ids[i]);
+
+ if (lookupTableIndex >= 0) {
+ foundNodes[i] = mViewNodeLookupTable.valueAt(lookupTableIndex);
+ } else {
+ missingNodeIndexes.put(i, /* ignored */ 0);
+ }
+ } else {
+ missingNodeIndexes.put(i, /* ignored */ 0);
+ }
+ }
+
+ final int numWindowNodes = mStructure.getWindowNodeCount();
+ for (int i = 0; i < numWindowNodes; i++) {
+ nodesToProcess.add(mStructure.getWindowNodeAt(i).getRootViewNode());
+ }
+
+ while (missingNodeIndexes.size() > 0 && !nodesToProcess.isEmpty()) {
+ final ViewNode node = nodesToProcess.removeFirst();
+
+ for (int i = 0; i < missingNodeIndexes.size(); i++) {
+ final int index = missingNodeIndexes.keyAt(i);
+ final AutofillId id = ids[index];
+
+ if (id.equals(node.getAutofillId())) {
+ foundNodes[index] = node;
+
+ if (mViewNodeLookupTable == null) {
+ mViewNodeLookupTable = new ArrayMap<>(ids.length);
+ }
+
+ mViewNodeLookupTable.put(id, node);
+
+ missingNodeIndexes.removeAt(i);
+ break;
+ }
+ }
+
+ for (int i = 0; i < node.getChildCount(); i++) {
+ nodesToProcess.addLast(node.getChildAt(i));
+ }
+ }
+
+ // Remember which ids could not be resolved to not search for them again the next time
+ for (int i = 0; i < missingNodeIndexes.size(); i++) {
+ if (mViewNodeLookupTable == null) {
+ mViewNodeLookupTable = new ArrayMap<>(missingNodeIndexes.size());
+ }
+
+ mViewNodeLookupTable.put(ids[missingNodeIndexes.keyAt(i)], null);
+ }
+
+ return foundNodes;
+ }
+
+
+
+ // Code below generated by codegen v1.0.0.
+ //
+ // DO NOT MODIFY!
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/FillContext.java
+ //
+ // CHECKSTYLE:OFF Generated code
+
+ /**
+ * Creates a new FillContext.
+ *
+ * @param requestId
+ * The id of the {@link FillRequest fill request} this context
+ * corresponds to. This is useful to associate your custom client
+ * state with every request to avoid reinterpreting the UI when saving
+ * user data.
+ * @param structure
+ * The screen content.
+ * @param focusedId
+ * The AutofillId of the view that triggered autofill.
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public FillContext(
+ int requestId,
+ @NonNull AssistStructure structure,
+ @NonNull AutofillId focusedId) {
+ this.mRequestId = requestId;
+ this.mStructure = structure;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mStructure);
+ this.mFocusedId = focusedId;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mFocusedId);
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * The id of the {@link FillRequest fill request} this context
+ * corresponds to. This is useful to associate your custom client
+ * state with every request to avoid reinterpreting the UI when saving
+ * user data.
+ */
+ @DataClass.Generated.Member
+ public int getRequestId() {
+ return mRequestId;
+ }
+
+ /**
+ * The screen content.
+ */
+ @DataClass.Generated.Member
+ public @NonNull AssistStructure getStructure() {
+ return mStructure;
+ }
+
+ /**
+ * The AutofillId of the view that triggered autofill.
+ */
+ @DataClass.Generated.Member
+ public @NonNull AutofillId getFocusedId() {
+ return mFocusedId;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ dest.writeInt(mRequestId);
+ dest.writeTypedObject(mStructure, flags);
+ dest.writeTypedObject(mFocusedId, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<FillContext> CREATOR
+ = new Parcelable.Creator<FillContext>() {
+ @Override
+ public FillContext[] newArray(int size) {
+ return new FillContext[size];
+ }
+
+ @Override
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ public FillContext createFromParcel(Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ int requestId = in.readInt();
+ AssistStructure structure = (AssistStructure) in.readTypedObject(AssistStructure.CREATOR);
+ AutofillId focusedId = (AutofillId) in.readTypedObject(AutofillId.CREATOR);
+ return new FillContext(
+ requestId,
+ structure,
+ focusedId);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1565152135263L,
+ codegenVersion = "1.0.0",
+ sourceFile = "frameworks/base/core/java/android/service/autofill/FillContext.java",
+ inputSignatures = "private final int mRequestId\nprivate final @android.annotation.NonNull android.app.assist.AssistStructure mStructure\nprivate final @android.annotation.NonNull android.view.autofill.AutofillId mFocusedId\nprivate transient @android.annotation.Nullable android.util.ArrayMap<android.view.autofill.AutofillId,android.app.assist.AssistStructure.ViewNode> mViewNodeLookupTable\npublic @java.lang.Override java.lang.String toString()\npublic @android.annotation.NonNull android.app.assist.AssistStructure.ViewNode[] findViewNodesByAutofillIds(android.view.autofill.AutofillId[])\nclass FillContext extends java.lang.Object implements [android.os.Parcelable]\[email protected](genHiddenConstructor=true, genAidl=false)")
+ @Deprecated
+ private void __metadata() {}
+
+}
diff --git a/android/service/autofill/FillEventHistory.java b/android/service/autofill/FillEventHistory.java
new file mode 100644
index 0000000..1cd2d62
--- /dev/null
+++ b/android/service/autofill/FillEventHistory.java
@@ -0,0 +1,580 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sVerbose;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Describes what happened after the last
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}
+ * call.
+ *
+ * <p>This history is typically used to keep track of previous user actions to optimize further
+ * requests. For example, the service might return email addresses in alphabetical order by
+ * default, but change that order based on the address the user picked on previous requests.
+ *
+ * <p>The history is not persisted over reboots, and it's cleared every time the service
+ * replies to a
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}
+ * by calling {@link FillCallback#onSuccess(FillResponse)} or
+ * {@link FillCallback#onFailure(CharSequence)} (if the service doesn't call any of these methods,
+ * the history will clear out after some pre-defined time).
+ */
+public final class FillEventHistory implements Parcelable {
+ private static final String TAG = "FillEventHistory";
+
+ /**
+ * Not in parcel. The ID of the autofill session that created the {@link FillResponse}.
+ */
+ private final int mSessionId;
+
+ @Nullable private final Bundle mClientState;
+ @Nullable List<Event> mEvents;
+
+ /** @hide */
+ public int getSessionId() {
+ return mSessionId;
+ }
+
+ /**
+ * Returns the client state set in the previous {@link FillResponse}.
+ *
+ * <p><b>Note: </b>the state is associated with the app that was autofilled in the previous
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}
+ * , which is not necessary the same app being autofilled now.
+ *
+ * @deprecated use {@link #getEvents()} then {@link Event#getClientState()} instead.
+ */
+ @Deprecated
+ @Nullable public Bundle getClientState() {
+ return mClientState;
+ }
+
+ /**
+ * Returns the events occurred after the latest call to
+ * {@link FillCallback#onSuccess(FillResponse)}.
+ *
+ * @return The list of events or {@code null} if non occurred.
+ */
+ @Nullable public List<Event> getEvents() {
+ return mEvents;
+ }
+
+ /**
+ * @hide
+ */
+ public void addEvent(Event event) {
+ if (mEvents == null) {
+ mEvents = new ArrayList<>(1);
+ }
+ mEvents.add(event);
+ }
+
+ /**
+ * @hide
+ */
+ public FillEventHistory(int sessionId, @Nullable Bundle clientState) {
+ mClientState = clientState;
+ mSessionId = sessionId;
+ }
+
+ @Override
+ public String toString() {
+ return mEvents == null ? "no events" : mEvents.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeBundle(mClientState);
+ if (mEvents == null) {
+ parcel.writeInt(0);
+ } else {
+ parcel.writeInt(mEvents.size());
+
+ int numEvents = mEvents.size();
+ for (int i = 0; i < numEvents; i++) {
+ Event event = mEvents.get(i);
+ parcel.writeInt(event.mEventType);
+ parcel.writeString(event.mDatasetId);
+ parcel.writeBundle(event.mClientState);
+ parcel.writeStringList(event.mSelectedDatasetIds);
+ parcel.writeArraySet(event.mIgnoredDatasetIds);
+ parcel.writeTypedList(event.mChangedFieldIds);
+ parcel.writeStringList(event.mChangedDatasetIds);
+
+ parcel.writeTypedList(event.mManuallyFilledFieldIds);
+ if (event.mManuallyFilledFieldIds != null) {
+ final int size = event.mManuallyFilledFieldIds.size();
+ for (int j = 0; j < size; j++) {
+ parcel.writeStringList(event.mManuallyFilledDatasetIds.get(j));
+ }
+ }
+ final AutofillId[] detectedFields = event.mDetectedFieldIds;
+ parcel.writeParcelableArray(detectedFields, flags);
+ if (detectedFields != null) {
+ FieldClassification.writeArrayToParcel(parcel,
+ event.mDetectedFieldClassifications);
+ }
+ }
+ }
+ }
+
+ /**
+ * Description of an event that occured after the latest call to
+ * {@link FillCallback#onSuccess(FillResponse)}.
+ */
+ public static final class Event {
+ /**
+ * A dataset was selected. The dataset selected can be read from {@link #getDatasetId()}.
+ *
+ * <p><b>Note: </b>on Android {@link android.os.Build.VERSION_CODES#O}, this event was also
+ * incorrectly reported after a
+ * {@link Dataset.Builder#setAuthentication(IntentSender) dataset authentication} was
+ * selected and the service returned a dataset in the
+ * {@link AutofillManager#EXTRA_AUTHENTICATION_RESULT} of the activity launched from that
+ * {@link IntentSender}. This behavior was fixed on Android
+ * {@link android.os.Build.VERSION_CODES#O_MR1}.
+ */
+ public static final int TYPE_DATASET_SELECTED = 0;
+
+ /**
+ * A {@link Dataset.Builder#setAuthentication(IntentSender) dataset authentication} was
+ * selected. The dataset authenticated can be read from {@link #getDatasetId()}.
+ */
+ public static final int TYPE_DATASET_AUTHENTICATION_SELECTED = 1;
+
+ /**
+ * A {@link FillResponse.Builder#setAuthentication(android.view.autofill.AutofillId[],
+ * IntentSender, android.widget.RemoteViews) fill response authentication} was selected.
+ */
+ public static final int TYPE_AUTHENTICATION_SELECTED = 2;
+
+ /** A save UI was shown. */
+ public static final int TYPE_SAVE_SHOWN = 3;
+
+ /**
+ * A committed autofill context for which the autofill service provided datasets.
+ *
+ * <p>This event is useful to track:
+ * <ul>
+ * <li>Which datasets (if any) were selected by the user
+ * ({@link #getSelectedDatasetIds()}).
+ * <li>Which datasets (if any) were NOT selected by the user
+ * ({@link #getIgnoredDatasetIds()}).
+ * <li>Which fields in the selected datasets were changed by the user after the dataset
+ * was selected ({@link #getChangedFields()}.
+ * <li>Which fields match the {@link UserData} set by the service.
+ * </ul>
+ *
+ * <p><b>Note: </b>This event is only generated when:
+ * <ul>
+ * <li>The autofill context is committed.
+ * <li>The service provides at least one dataset in the
+ * {@link FillResponse fill responses} associated with the context.
+ * <li>The last {@link FillResponse fill responses} associated with the context has the
+ * {@link FillResponse#FLAG_TRACK_CONTEXT_COMMITED} flag.
+ * </ul>
+ *
+ * <p>See {@link android.view.autofill.AutofillManager} for more information about autofill
+ * contexts.
+ */
+ public static final int TYPE_CONTEXT_COMMITTED = 4;
+
+ /**
+ * A dataset selector was shown.
+ *
+ * <p>This event is fired whenever the autofill UI was presented to the user.</p>
+ */
+ public static final int TYPE_DATASETS_SHOWN = 5;
+
+ /** @hide */
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_DATASET_SELECTED,
+ TYPE_DATASET_AUTHENTICATION_SELECTED,
+ TYPE_AUTHENTICATION_SELECTED,
+ TYPE_SAVE_SHOWN,
+ TYPE_CONTEXT_COMMITTED,
+ TYPE_DATASETS_SHOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface EventIds{}
+
+ @EventIds private final int mEventType;
+ @Nullable private final String mDatasetId;
+ @Nullable private final Bundle mClientState;
+
+ // Note: mSelectedDatasetIds is stored as List<> instead of Set because Session already
+ // stores it as List
+ @Nullable private final List<String> mSelectedDatasetIds;
+ @Nullable private final ArraySet<String> mIgnoredDatasetIds;
+
+ @Nullable private final ArrayList<AutofillId> mChangedFieldIds;
+ @Nullable private final ArrayList<String> mChangedDatasetIds;
+
+ @Nullable private final ArrayList<AutofillId> mManuallyFilledFieldIds;
+ @Nullable private final ArrayList<ArrayList<String>> mManuallyFilledDatasetIds;
+
+ @Nullable private final AutofillId[] mDetectedFieldIds;
+ @Nullable private final FieldClassification[] mDetectedFieldClassifications;
+
+ /**
+ * Returns the type of the event.
+ *
+ * @return The type of the event
+ */
+ public int getType() {
+ return mEventType;
+ }
+
+ /**
+ * Returns the id of dataset the id was on.
+ *
+ * @return The id of dataset, or {@code null} the event is not associated with a dataset.
+ */
+ @Nullable public String getDatasetId() {
+ return mDatasetId;
+ }
+
+ /**
+ * Returns the client state from the {@link FillResponse} used to generate this event.
+ *
+ * <p><b>Note: </b>the state is associated with the app that was autofilled in the previous
+ * {@link
+ * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)},
+ * which is not necessary the same app being autofilled now.
+ */
+ @Nullable public Bundle getClientState() {
+ return mClientState;
+ }
+
+ /**
+ * Returns which datasets were selected by the user.
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ */
+ @NonNull public Set<String> getSelectedDatasetIds() {
+ return mSelectedDatasetIds == null ? Collections.emptySet()
+ : new ArraySet<>(mSelectedDatasetIds);
+ }
+
+ /**
+ * Returns which datasets were NOT selected by the user.
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ */
+ @NonNull public Set<String> getIgnoredDatasetIds() {
+ return mIgnoredDatasetIds == null ? Collections.emptySet() : mIgnoredDatasetIds;
+ }
+
+ /**
+ * Returns which fields in the selected datasets were changed by the user after the dataset
+ * was selected.
+ *
+ * <p>For example, server provides:
+ *
+ * <pre class="prettyprint">
+ * FillResponse response = new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder(presentation1)
+ * .setId("4815")
+ * .setValue(usernameId, AutofillValue.forText("MrPlow"))
+ * .build())
+ * .addDataset(new Dataset.Builder(presentation2)
+ * .setId("162342")
+ * .setValue(passwordId, AutofillValue.forText("D'OH"))
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>User select both datasets (for username and password) but after the fields are
+ * autofilled, user changes them to:
+ *
+ * <pre class="prettyprint">
+ * username = "ElBarto";
+ * password = "AyCaramba";
+ * </pre>
+ *
+ * <p>Then the result is the following map:
+ *
+ * <pre class="prettyprint">
+ * usernameId => "4815"
+ * passwordId => "162342"
+ * </pre>
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ *
+ * @return map map whose key is the id of the change fields, and value is the id of
+ * dataset that has that field and was selected by the user.
+ */
+ @NonNull public Map<AutofillId, String> getChangedFields() {
+ if (mChangedFieldIds == null || mChangedDatasetIds == null) {
+ return Collections.emptyMap();
+ }
+
+ final int size = mChangedFieldIds.size();
+ final ArrayMap<AutofillId, String> changedFields = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ changedFields.put(mChangedFieldIds.get(i), mChangedDatasetIds.get(i));
+ }
+ return changedFields;
+ }
+
+ /**
+ * Gets the <a href="AutofillService.html#FieldClassification">field classification</a>
+ * results.
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the
+ * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)
+ * field classification}.
+ */
+ @NonNull public Map<AutofillId, FieldClassification> getFieldsClassification() {
+ if (mDetectedFieldIds == null) {
+ return Collections.emptyMap();
+ }
+ final int size = mDetectedFieldIds.length;
+ final ArrayMap<AutofillId, FieldClassification> map = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ final AutofillId id = mDetectedFieldIds[i];
+ final FieldClassification fc = mDetectedFieldClassifications[i];
+ if (sVerbose) {
+ Log.v(TAG, "getFieldsClassification[" + i + "]: id=" + id + ", fc=" + fc);
+ }
+ map.put(id, fc);
+ }
+ return map;
+ }
+
+ /**
+ * Returns which fields were available on datasets provided by the service but manually
+ * entered by the user.
+ *
+ * <p>For example, server provides:
+ *
+ * <pre class="prettyprint">
+ * FillResponse response = new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder(presentation1)
+ * .setId("4815")
+ * .setValue(usernameId, AutofillValue.forText("MrPlow"))
+ * .setValue(passwordId, AutofillValue.forText("AyCaramba"))
+ * .build())
+ * .addDataset(new Dataset.Builder(presentation2)
+ * .setId("162342")
+ * .setValue(usernameId, AutofillValue.forText("ElBarto"))
+ * .setValue(passwordId, AutofillValue.forText("D'OH"))
+ * .build())
+ * .addDataset(new Dataset.Builder(presentation3)
+ * .setId("108")
+ * .setValue(usernameId, AutofillValue.forText("MrPlow"))
+ * .setValue(passwordId, AutofillValue.forText("D'OH"))
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>User doesn't select a dataset but manually enters:
+ *
+ * <pre class="prettyprint">
+ * username = "MrPlow";
+ * password = "D'OH";
+ * </pre>
+ *
+ * <p>Then the result is the following map:
+ *
+ * <pre class="prettyprint">
+ * usernameId => { "4815", "108"}
+ * passwordId => { "162342", "108" }
+ * </pre>
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ *
+ * @return map map whose key is the id of the manually-entered field, and value is the
+ * ids of the datasets that have that value but were not selected by the user.
+ */
+ @NonNull public Map<AutofillId, Set<String>> getManuallyEnteredField() {
+ if (mManuallyFilledFieldIds == null || mManuallyFilledDatasetIds == null) {
+ return Collections.emptyMap();
+ }
+
+ final int size = mManuallyFilledFieldIds.size();
+ final Map<AutofillId, Set<String>> manuallyFilledFields = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ final AutofillId fieldId = mManuallyFilledFieldIds.get(i);
+ final ArrayList<String> datasetIds = mManuallyFilledDatasetIds.get(i);
+ manuallyFilledFields.put(fieldId, new ArraySet<>(datasetIds));
+ }
+ return manuallyFilledFields;
+ }
+
+ /**
+ * Creates a new event.
+ *
+ * @param eventType The type of the event
+ * @param datasetId The dataset the event was on, or {@code null} if the event was on the
+ * whole response.
+ * @param clientState The client state associated with the event.
+ * @param selectedDatasetIds The ids of datasets selected by the user.
+ * @param ignoredDatasetIds The ids of datasets NOT select by the user.
+ * @param changedFieldIds The ids of fields changed by the user.
+ * @param changedDatasetIds The ids of the datasets that havd values matching the
+ * respective entry on {@code changedFieldIds}.
+ * @param manuallyFilledFieldIds The ids of fields that were manually entered by the user
+ * and belonged to datasets.
+ * @param manuallyFilledDatasetIds The ids of datasets that had values matching the
+ * respective entry on {@code manuallyFilledFieldIds}.
+ * @param detectedFieldClassifications the field classification matches.
+ *
+ * @throws IllegalArgumentException If the length of {@code changedFieldIds} and
+ * {@code changedDatasetIds} doesn't match.
+ * @throws IllegalArgumentException If the length of {@code manuallyFilledFieldIds} and
+ * {@code manuallyFilledDatasetIds} doesn't match.
+ *
+ * @hide
+ */
+ public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState,
+ @Nullable List<String> selectedDatasetIds,
+ @Nullable ArraySet<String> ignoredDatasetIds,
+ @Nullable ArrayList<AutofillId> changedFieldIds,
+ @Nullable ArrayList<String> changedDatasetIds,
+ @Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
+ @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
+ @Nullable AutofillId[] detectedFieldIds,
+ @Nullable FieldClassification[] detectedFieldClassifications) {
+ mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_DATASETS_SHOWN,
+ "eventType");
+ mDatasetId = datasetId;
+ mClientState = clientState;
+ mSelectedDatasetIds = selectedDatasetIds;
+ mIgnoredDatasetIds = ignoredDatasetIds;
+ if (changedFieldIds != null) {
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(changedFieldIds)
+ && changedDatasetIds != null
+ && changedFieldIds.size() == changedDatasetIds.size(),
+ "changed ids must have same length and not be empty");
+ }
+ mChangedFieldIds = changedFieldIds;
+ mChangedDatasetIds = changedDatasetIds;
+ if (manuallyFilledFieldIds != null) {
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(manuallyFilledFieldIds)
+ && manuallyFilledDatasetIds != null
+ && manuallyFilledFieldIds.size() == manuallyFilledDatasetIds.size(),
+ "manually filled ids must have same length and not be empty");
+ }
+ mManuallyFilledFieldIds = manuallyFilledFieldIds;
+ mManuallyFilledDatasetIds = manuallyFilledDatasetIds;
+
+ mDetectedFieldIds = detectedFieldIds;
+ mDetectedFieldClassifications = detectedFieldClassifications;
+ }
+
+ @Override
+ public String toString() {
+ return "FillEvent [datasetId=" + mDatasetId
+ + ", type=" + mEventType
+ + ", selectedDatasets=" + mSelectedDatasetIds
+ + ", ignoredDatasetIds=" + mIgnoredDatasetIds
+ + ", changedFieldIds=" + mChangedFieldIds
+ + ", changedDatasetsIds=" + mChangedDatasetIds
+ + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
+ + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
+ + ", detectedFieldIds=" + Arrays.toString(mDetectedFieldIds)
+ + ", detectedFieldClassifications ="
+ + Arrays.toString(mDetectedFieldClassifications)
+ + "]";
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<FillEventHistory> CREATOR =
+ new Parcelable.Creator<FillEventHistory>() {
+ @Override
+ public FillEventHistory createFromParcel(Parcel parcel) {
+ FillEventHistory selection = new FillEventHistory(0, parcel.readBundle());
+
+ final int numEvents = parcel.readInt();
+ for (int i = 0; i < numEvents; i++) {
+ final int eventType = parcel.readInt();
+ final String datasetId = parcel.readString();
+ final Bundle clientState = parcel.readBundle();
+ final ArrayList<String> selectedDatasetIds = parcel.createStringArrayList();
+ @SuppressWarnings("unchecked")
+ final ArraySet<String> ignoredDatasets =
+ (ArraySet<String>) parcel.readArraySet(null);
+ final ArrayList<AutofillId> changedFieldIds =
+ parcel.createTypedArrayList(AutofillId.CREATOR);
+ final ArrayList<String> changedDatasetIds = parcel.createStringArrayList();
+
+ final ArrayList<AutofillId> manuallyFilledFieldIds =
+ parcel.createTypedArrayList(AutofillId.CREATOR);
+ final ArrayList<ArrayList<String>> manuallyFilledDatasetIds;
+ if (manuallyFilledFieldIds != null) {
+ final int size = manuallyFilledFieldIds.size();
+ manuallyFilledDatasetIds = new ArrayList<>(size);
+ for (int j = 0; j < size; j++) {
+ manuallyFilledDatasetIds.add(parcel.createStringArrayList());
+ }
+ } else {
+ manuallyFilledDatasetIds = null;
+ }
+ final AutofillId[] detectedFieldIds = parcel.readParcelableArray(null,
+ AutofillId.class);
+ final FieldClassification[] detectedFieldClassifications =
+ (detectedFieldIds != null)
+ ? FieldClassification.readArrayFromParcel(parcel)
+ : null;
+
+ selection.addEvent(new Event(eventType, datasetId, clientState,
+ selectedDatasetIds, ignoredDatasets,
+ changedFieldIds, changedDatasetIds,
+ manuallyFilledFieldIds, manuallyFilledDatasetIds,
+ detectedFieldIds, detectedFieldClassifications));
+ }
+ return selection;
+ }
+
+ @Override
+ public FillEventHistory[] newArray(int size) {
+ return new FillEventHistory[size];
+ }
+ };
+}
diff --git a/android/service/autofill/FillRequest.java b/android/service/autofill/FillRequest.java
new file mode 100644
index 0000000..d94160c
--- /dev/null
+++ b/android/service/autofill/FillRequest.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.View;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import com.android.internal.util.DataClass;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents a request to an autofill service
+ * to interpret the screen and provide information to the system which views are
+ * interesting for saving and what are the possible ways to fill the inputs on
+ * the screen if applicable.
+ *
+ * @see AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)
+ */
+@DataClass(
+ genToString = true,
+ genHiddenConstructor = true,
+ genHiddenConstDefs = true)
+public final class FillRequest implements Parcelable {
+
+ /**
+ * Indicates autofill was explicitly requested by the user.
+ *
+ * <p>Users typically make an explicit request to autofill a screen in two situations:
+ * <ul>
+ * <li>The app disabled autofill (using {@link View#setImportantForAutofill(int)}.
+ * <li>The service could not figure out how to autofill a screen (but the user knows the
+ * service has data for that app).
+ * </ul>
+ *
+ * <p>This flag is particularly useful for the second case. For example, the service could offer
+ * a complex UI where the user can map which screen views belong to each user data, or it could
+ * offer a simpler UI where the user picks the data for just the view used to trigger the
+ * request (that would be the view whose
+ * {@link android.app.assist.AssistStructure.ViewNode#isFocused()} method returns {@code true}).
+ *
+ * <p>An explicit autofill request is triggered when the
+ * {@link android.view.autofill.AutofillManager#requestAutofill(View)} or
+ * {@link android.view.autofill.AutofillManager#requestAutofill(View, int, android.graphics.Rect)}
+ * is called. For example, standard {@link android.widget.TextView} views show an
+ * {@code AUTOFILL} option in the overflow menu that triggers such request.
+ */
+ public static final @RequestFlags int FLAG_MANUAL_REQUEST = 0x1;
+
+ /**
+ * Indicates this request was made using
+ * <a href="AutofillService.html#CompatibilityMode">compatibility mode</a>.
+ */
+ public static final @RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST = 0x2;
+
+ /**
+ * Indicates the request came from a password field.
+ *
+ * (TODO: b/141703197) Temporary fix for augmented autofill showing passwords.
+ *
+ * @hide
+ */
+ public static final @RequestFlags int FLAG_PASSWORD_INPUT_TYPE = 0x4;
+
+ /** @hide */
+ public static final int INVALID_REQUEST_ID = Integer.MIN_VALUE;
+
+ /**
+ * Gets the unique id of this request.
+ */
+ private final int mId;
+
+ /**
+ * Gets the contexts associated with each previous fill request.
+ *
+ * <p><b>Note:</b> Starting on Android {@link android.os.Build.VERSION_CODES#Q}, it could also
+ * include contexts from requests whose {@link SaveInfo} had the
+ * {@link SaveInfo#FLAG_DELAY_SAVE} flag.
+ */
+ private final @NonNull List<FillContext> mFillContexts;
+
+ /**
+ * Gets the latest client state bundle set by the service in a
+ * {@link FillResponse.Builder#setClientState(Bundle) fill response}.
+ *
+ * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state
+ * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On
+ * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of
+ * an authenticated request through the
+ * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are
+ * also considered (and take precedence when set).
+ *
+ * @return The client state.
+ */
+ private final @Nullable Bundle mClientState;
+
+ /**
+ * Gets the flags associated with this request.
+ *
+ * @return any combination of {@link #FLAG_MANUAL_REQUEST} and
+ * {@link #FLAG_COMPATIBILITY_MODE_REQUEST}.
+ */
+ private final @RequestFlags int mFlags;
+
+ /**
+ * Gets the {@link InlineSuggestionsRequest} associated
+ * with this request.
+ *
+ * <p>Autofill Framework will send a {@code @non-null} {@link InlineSuggestionsRequest} if
+ * currently inline suggestions are supported and can be displayed. If the Autofill service
+ * wants to show inline suggestions, they may return {@link Dataset} with valid
+ * {@link InlinePresentation}.</p>
+ *
+ * <p>The Autofill Service must set supportsInlineSuggestions in its XML to enable support
+ * for inline suggestions.</p>
+ *
+ * @return the suggestionspec
+ */
+ private final @Nullable InlineSuggestionsRequest mInlineSuggestionsRequest;
+
+ private void onConstructed() {
+ Preconditions.checkCollectionElementsNotNull(mFillContexts, "contexts");
+ }
+
+
+
+ // Code below generated by codegen v1.0.15.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/FillRequest.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /** @hide */
+ @IntDef(flag = true, prefix = "FLAG_", value = {
+ FLAG_MANUAL_REQUEST,
+ FLAG_COMPATIBILITY_MODE_REQUEST,
+ FLAG_PASSWORD_INPUT_TYPE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @DataClass.Generated.Member
+ public @interface RequestFlags {}
+
+ /** @hide */
+ @DataClass.Generated.Member
+ public static String requestFlagsToString(@RequestFlags int value) {
+ return com.android.internal.util.BitUtils.flagsToString(
+ value, FillRequest::singleRequestFlagsToString);
+ }
+
+ @DataClass.Generated.Member
+ static String singleRequestFlagsToString(@RequestFlags int value) {
+ switch (value) {
+ case FLAG_MANUAL_REQUEST:
+ return "FLAG_MANUAL_REQUEST";
+ case FLAG_COMPATIBILITY_MODE_REQUEST:
+ return "FLAG_COMPATIBILITY_MODE_REQUEST";
+ case FLAG_PASSWORD_INPUT_TYPE:
+ return "FLAG_PASSWORD_INPUT_TYPE";
+ default: return Integer.toHexString(value);
+ }
+ }
+
+ /**
+ * Creates a new FillRequest.
+ *
+ * @param id
+ * Gets the unique id of this request.
+ * @param fillContexts
+ * Gets the contexts associated with each previous fill request.
+ *
+ * <p><b>Note:</b> Starting on Android {@link android.os.Build.VERSION_CODES#Q}, it could also
+ * include contexts from requests whose {@link SaveInfo} had the
+ * {@link SaveInfo#FLAG_DELAY_SAVE} flag.
+ * @param clientState
+ * Gets the latest client state bundle set by the service in a
+ * {@link FillResponse.Builder#setClientState(Bundle) fill response}.
+ *
+ * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state
+ * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On
+ * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of
+ * an authenticated request through the
+ * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are
+ * also considered (and take precedence when set).
+ * @param flags
+ * Gets the flags associated with this request.
+ *
+ * @return any combination of {@link #FLAG_MANUAL_REQUEST} and
+ * {@link #FLAG_COMPATIBILITY_MODE_REQUEST}.
+ * @param inlineSuggestionsRequest
+ * Gets the {@link InlineSuggestionsRequest} associated
+ * with this request.
+ *
+ * <p>Autofill Framework will send a {@code @non-null} {@link InlineSuggestionsRequest} if
+ * currently inline suggestions are supported and can be displayed. If the Autofill service
+ * wants to show inline suggestions, they may return {@link Dataset} with valid
+ * {@link InlinePresentation}.</p>
+ *
+ * <p>The Autofill Service must set supportsInlineSuggestions in its XML to enable support
+ * for inline suggestions.</p>
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public FillRequest(
+ int id,
+ @NonNull List<FillContext> fillContexts,
+ @Nullable Bundle clientState,
+ @RequestFlags int flags,
+ @Nullable InlineSuggestionsRequest inlineSuggestionsRequest) {
+ this.mId = id;
+ this.mFillContexts = fillContexts;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mFillContexts);
+ this.mClientState = clientState;
+ this.mFlags = flags;
+
+ Preconditions.checkFlagsArgument(
+ mFlags,
+ FLAG_MANUAL_REQUEST
+ | FLAG_COMPATIBILITY_MODE_REQUEST
+ | FLAG_PASSWORD_INPUT_TYPE);
+ this.mInlineSuggestionsRequest = inlineSuggestionsRequest;
+
+ onConstructed();
+ }
+
+ /**
+ * Gets the unique id of this request.
+ */
+ @DataClass.Generated.Member
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Gets the contexts associated with each previous fill request.
+ *
+ * <p><b>Note:</b> Starting on Android {@link android.os.Build.VERSION_CODES#Q}, it could also
+ * include contexts from requests whose {@link SaveInfo} had the
+ * {@link SaveInfo#FLAG_DELAY_SAVE} flag.
+ */
+ @DataClass.Generated.Member
+ public @NonNull List<FillContext> getFillContexts() {
+ return mFillContexts;
+ }
+
+ /**
+ * Gets the latest client state bundle set by the service in a
+ * {@link FillResponse.Builder#setClientState(Bundle) fill response}.
+ *
+ * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state
+ * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On
+ * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of
+ * an authenticated request through the
+ * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are
+ * also considered (and take precedence when set).
+ *
+ * @return The client state.
+ */
+ @DataClass.Generated.Member
+ public @Nullable Bundle getClientState() {
+ return mClientState;
+ }
+
+ /**
+ * Gets the flags associated with this request.
+ *
+ * @return any combination of {@link #FLAG_MANUAL_REQUEST} and
+ * {@link #FLAG_COMPATIBILITY_MODE_REQUEST}.
+ */
+ @DataClass.Generated.Member
+ public @RequestFlags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Gets the {@link InlineSuggestionsRequest} associated
+ * with this request.
+ *
+ * <p>Autofill Framework will send a {@code @non-null} {@link InlineSuggestionsRequest} if
+ * currently inline suggestions are supported and can be displayed. If the Autofill service
+ * wants to show inline suggestions, they may return {@link Dataset} with valid
+ * {@link InlinePresentation}.</p>
+ *
+ * <p>The Autofill Service must set supportsInlineSuggestions in its XML to enable support
+ * for inline suggestions.</p>
+ *
+ * @return the suggestionspec
+ */
+ @DataClass.Generated.Member
+ public @Nullable InlineSuggestionsRequest getInlineSuggestionsRequest() {
+ return mInlineSuggestionsRequest;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "FillRequest { " +
+ "id = " + mId + ", " +
+ "fillContexts = " + mFillContexts + ", " +
+ "clientState = " + mClientState + ", " +
+ "flags = " + requestFlagsToString(mFlags) + ", " +
+ "inlineSuggestionsRequest = " + mInlineSuggestionsRequest +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mClientState != null) flg |= 0x4;
+ if (mInlineSuggestionsRequest != null) flg |= 0x10;
+ dest.writeByte(flg);
+ dest.writeInt(mId);
+ dest.writeParcelableList(mFillContexts, flags);
+ if (mClientState != null) dest.writeBundle(mClientState);
+ dest.writeInt(mFlags);
+ if (mInlineSuggestionsRequest != null) dest.writeTypedObject(mInlineSuggestionsRequest, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ FillRequest(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ int id = in.readInt();
+ List<FillContext> fillContexts = new ArrayList<>();
+ in.readParcelableList(fillContexts, FillContext.class.getClassLoader());
+ Bundle clientState = (flg & 0x4) == 0 ? null : in.readBundle();
+ int flags = in.readInt();
+ InlineSuggestionsRequest inlineSuggestionsRequest = (flg & 0x10) == 0 ? null : (InlineSuggestionsRequest) in.readTypedObject(InlineSuggestionsRequest.CREATOR);
+
+ this.mId = id;
+ this.mFillContexts = fillContexts;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mFillContexts);
+ this.mClientState = clientState;
+ this.mFlags = flags;
+
+ Preconditions.checkFlagsArgument(
+ mFlags,
+ FLAG_MANUAL_REQUEST
+ | FLAG_COMPATIBILITY_MODE_REQUEST
+ | FLAG_PASSWORD_INPUT_TYPE);
+ this.mInlineSuggestionsRequest = inlineSuggestionsRequest;
+
+ onConstructed();
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<FillRequest> CREATOR
+ = new Parcelable.Creator<FillRequest>() {
+ @Override
+ public FillRequest[] newArray(int size) {
+ return new FillRequest[size];
+ }
+
+ @Override
+ public FillRequest createFromParcel(@NonNull Parcel in) {
+ return new FillRequest(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1588119440090L,
+ codegenVersion = "1.0.15",
+ sourceFile = "frameworks/base/core/java/android/service/autofill/FillRequest.java",
+ inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\[email protected](genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/service/autofill/FillResponse.java b/android/service/autofill/FillResponse.java
new file mode 100644
index 0000000..bc08b84
--- /dev/null
+++ b/android/service/autofill/FillResponse.java
@@ -0,0 +1,863 @@
+/*
+ * Copyright (C) 2016 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.autofill;
+
+import static android.service.autofill.AutofillServiceHelper.assertValid;
+import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.app.Activity;
+import android.content.IntentSender;
+import android.content.pm.ParceledListSlice;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.autofill.AutofillId;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Response for an {@link
+ * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}.
+ *
+ * <p>See the main {@link AutofillService} documentation for more details and examples.
+ */
+public final class FillResponse implements Parcelable {
+
+ /**
+ * Flag used to generate {@link FillEventHistory.Event events} of type
+ * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}—if this flag is not passed to
+ * {@link Builder#setFlags(int)}, these events are not generated.
+ */
+ public static final int FLAG_TRACK_CONTEXT_COMMITED = 0x1;
+
+ /**
+ * Flag used to change the behavior of {@link FillResponse.Builder#disableAutofill(long)}—
+ * when this flag is passed to {@link Builder#setFlags(int)}, autofill is disabled only for the
+ * activiy that generated the {@link FillRequest}, not the whole app.
+ */
+ public static final int FLAG_DISABLE_ACTIVITY_ONLY = 0x2;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_TRACK_CONTEXT_COMMITED,
+ FLAG_DISABLE_ACTIVITY_ONLY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface FillResponseFlags {}
+
+ private final @Nullable ParceledListSlice<Dataset> mDatasets;
+ private final @Nullable SaveInfo mSaveInfo;
+ private final @Nullable Bundle mClientState;
+ private final @Nullable RemoteViews mPresentation;
+ private final @Nullable InlinePresentation mInlinePresentation;
+ private final @Nullable RemoteViews mHeader;
+ private final @Nullable RemoteViews mFooter;
+ private final @Nullable IntentSender mAuthentication;
+ private final @Nullable AutofillId[] mAuthenticationIds;
+ private final @Nullable AutofillId[] mIgnoredIds;
+ private final long mDisableDuration;
+ private final @Nullable AutofillId[] mFieldClassificationIds;
+ private final int mFlags;
+ private int mRequestId;
+ private final @Nullable UserData mUserData;
+ private final @Nullable int[] mCancelIds;
+ private final boolean mSupportsInlineSuggestions;
+
+ private FillResponse(@NonNull Builder builder) {
+ mDatasets = (builder.mDatasets != null) ? new ParceledListSlice<>(builder.mDatasets) : null;
+ mSaveInfo = builder.mSaveInfo;
+ mClientState = builder.mClientState;
+ mPresentation = builder.mPresentation;
+ mInlinePresentation = builder.mInlinePresentation;
+ mHeader = builder.mHeader;
+ mFooter = builder.mFooter;
+ mAuthentication = builder.mAuthentication;
+ mAuthenticationIds = builder.mAuthenticationIds;
+ mIgnoredIds = builder.mIgnoredIds;
+ mDisableDuration = builder.mDisableDuration;
+ mFieldClassificationIds = builder.mFieldClassificationIds;
+ mFlags = builder.mFlags;
+ mRequestId = INVALID_REQUEST_ID;
+ mUserData = builder.mUserData;
+ mCancelIds = builder.mCancelIds;
+ mSupportsInlineSuggestions = builder.mSupportsInlineSuggestions;
+ }
+
+ /** @hide */
+ public @Nullable Bundle getClientState() {
+ return mClientState;
+ }
+
+ /** @hide */
+ public @Nullable List<Dataset> getDatasets() {
+ return (mDatasets != null) ? mDatasets.getList() : null;
+ }
+
+ /** @hide */
+ public @Nullable SaveInfo getSaveInfo() {
+ return mSaveInfo;
+ }
+
+ /** @hide */
+ public @Nullable RemoteViews getPresentation() {
+ return mPresentation;
+ }
+
+ /** @hide */
+ public @Nullable InlinePresentation getInlinePresentation() {
+ return mInlinePresentation;
+ }
+
+ /** @hide */
+ public @Nullable RemoteViews getHeader() {
+ return mHeader;
+ }
+
+ /** @hide */
+ public @Nullable RemoteViews getFooter() {
+ return mFooter;
+ }
+
+ /** @hide */
+ public @Nullable IntentSender getAuthentication() {
+ return mAuthentication;
+ }
+
+ /** @hide */
+ public @Nullable AutofillId[] getAuthenticationIds() {
+ return mAuthenticationIds;
+ }
+
+ /** @hide */
+ public @Nullable AutofillId[] getIgnoredIds() {
+ return mIgnoredIds;
+ }
+
+ /** @hide */
+ public long getDisableDuration() {
+ return mDisableDuration;
+ }
+
+ /** @hide */
+ public @Nullable AutofillId[] getFieldClassificationIds() {
+ return mFieldClassificationIds;
+ }
+
+ /** @hide */
+ public @Nullable UserData getUserData() {
+ return mUserData;
+ }
+
+ /** @hide */
+ @TestApi
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Associates a {@link FillResponse} to a request.
+ *
+ * <p>Set inside of the {@link FillCallback} code, not the {@link AutofillService}.
+ *
+ * @param requestId The id of the request to associate the response to.
+ *
+ * @hide
+ */
+ public void setRequestId(int requestId) {
+ mRequestId = requestId;
+ }
+
+ /** @hide */
+ public int getRequestId() {
+ return mRequestId;
+ }
+
+ /** @hide */
+ @Nullable
+ public int[] getCancelIds() {
+ return mCancelIds;
+ }
+
+ /** @hide */
+ public boolean supportsInlineSuggestions() {
+ return mSupportsInlineSuggestions;
+ }
+
+ /**
+ * Builder for {@link FillResponse} objects. You must to provide at least
+ * one dataset or set an authentication intent with a presentation view.
+ */
+ public static final class Builder {
+ private ArrayList<Dataset> mDatasets;
+ private SaveInfo mSaveInfo;
+ private Bundle mClientState;
+ private RemoteViews mPresentation;
+ private InlinePresentation mInlinePresentation;
+ private RemoteViews mHeader;
+ private RemoteViews mFooter;
+ private IntentSender mAuthentication;
+ private AutofillId[] mAuthenticationIds;
+ private AutofillId[] mIgnoredIds;
+ private long mDisableDuration;
+ private AutofillId[] mFieldClassificationIds;
+ private int mFlags;
+ private boolean mDestroyed;
+ private UserData mUserData;
+ private int[] mCancelIds;
+ private boolean mSupportsInlineSuggestions;
+
+ /**
+ * Triggers a custom UI before before autofilling the screen with any data set in this
+ * response.
+ *
+ * <p><b>Note:</b> Although the name of this method suggests that it should be used just for
+ * authentication flow, it can be used for other advanced flows; see {@link AutofillService}
+ * for examples.
+ *
+ * <p>This is typically useful when a user interaction is required to unlock their
+ * data vault if you encrypt the data set labels and data set data. It is recommended
+ * to encrypt only the sensitive data and not the data set labels which would allow
+ * auth on the data set level leading to a better user experience. Note that if you
+ * use sensitive data as a label, for example an email address, then it should also
+ * be encrypted. The provided {@link android.app.PendingIntent intent} must be an
+ * {@link Activity} which implements your authentication flow. Also if you provide an auth
+ * intent you also need to specify the presentation view to be shown in the fill UI
+ * for the user to trigger your authentication flow.
+ *
+ * <p>When a user triggers autofill, the system launches the provided intent
+ * whose extras will have the
+ * {@link android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen
+ * content} and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE
+ * client state}. Once you complete your authentication flow you should set the
+ * {@link Activity} result to {@link android.app.Activity#RESULT_OK} and set the
+ * {@link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra
+ * with the fully populated {@link FillResponse response} (or {@code null} if the screen
+ * cannot be autofilled).
+ *
+ * <p>For example, if you provided an empty {@link FillResponse response} because the
+ * user's data was locked and marked that the response needs an authentication then
+ * in the response returned if authentication succeeds you need to provide all
+ * available data sets some of which may need to be further authenticated, for
+ * example a credit card whose CVV needs to be entered.
+ *
+ * <p>If you provide an authentication intent you must also provide a presentation
+ * which is used to visualize visualize the response for triggering the authentication
+ * flow.
+ *
+ * <p><b>Note:</b> Do not make the provided pending intent
+ * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
+ * platform needs to fill in the authentication arguments.
+ *
+ * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
+ * or background color: Autofill on different platforms may have different themes.
+ *
+ * @param authentication Intent to an activity with your authentication flow.
+ * @param presentation The presentation to visualize the response.
+ * @param ids id of Views that when focused will display the authentication UI.
+ *
+ * @return This builder.
+ *
+ * @throws IllegalArgumentException if any of the following occurs:
+ * <ul>
+ * <li>{@code ids} is {@code null}</li>
+ * <li>{@code ids} is empty</li>
+ * <li>{@code ids} contains a {@code null} element</li>
+ * <li>both {@code authentication} and {@code presentation} are {@code null}</li>
+ * <li>both {@code authentication} and {@code presentation} are non-{@code null}</li>
+ * </ul>
+ *
+ * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a
+ * {@link #setFooter(RemoteViews) footer} are already set for this builder.
+ *
+ * @see android.app.PendingIntent#getIntentSender()
+ */
+ @NonNull
+ public Builder setAuthentication(@NonNull AutofillId[] ids,
+ @Nullable IntentSender authentication, @Nullable RemoteViews presentation) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ if (mHeader != null || mFooter != null) {
+ throw new IllegalStateException("Already called #setHeader() or #setFooter()");
+ }
+
+ if (authentication == null ^ presentation == null) {
+ throw new IllegalArgumentException("authentication and presentation"
+ + " must be both non-null or null");
+ }
+ mAuthentication = authentication;
+ mPresentation = presentation;
+ mAuthenticationIds = assertValid(ids);
+ return this;
+ }
+
+ /**
+ * Triggers a custom UI before before autofilling the screen with any data set in this
+ * response.
+ *
+ * <p><b>Note:</b> Although the name of this method suggests that it should be used just for
+ * authentication flow, it can be used for other advanced flows; see {@link AutofillService}
+ * for examples.
+ *
+ * <p>This method is similar to
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, but also accepts
+ * an {@link InlinePresentation} presentation which is required for authenticating through
+ * the inline autofill flow.
+ *
+ * <p><b>Note:</b> {@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} does
+ * not work with {@link InlinePresentation}.</p>
+ *
+ * @param authentication Intent to an activity with your authentication flow.
+ * @param presentation The presentation to visualize the response.
+ * @param inlinePresentation The inlinePresentation to visualize the response inline.
+ * @param ids id of Views that when focused will display the authentication UI.
+ *
+ * @return This builder.
+ *
+ * @throws IllegalArgumentException if any of the following occurs:
+ * <ul>
+ * <li>{@code ids} is {@code null}</li>
+ * <li>{@code ids} is empty</li>
+ * <li>{@code ids} contains a {@code null} element</li>
+ * <li>both {@code authentication} and {@code presentation} are {@code null}</li>
+ * <li>both {@code authentication} and {@code presentation} are non-{@code null}</li>
+ * <li>both {@code authentication} and {@code inlinePresentation} are {@code null}</li>
+ * <li>both {@code authentication} and {@code inlinePresentation} are
+ * non-{@code null}</li>
+ * </ul>
+ *
+ * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a
+ * {@link #setFooter(RemoteViews) footer} are already set for this builder.
+ *
+ * @see android.app.PendingIntent#getIntentSender()
+ */
+ @NonNull
+ public Builder setAuthentication(@NonNull AutofillId[] ids,
+ @Nullable IntentSender authentication, @Nullable RemoteViews presentation,
+ @Nullable InlinePresentation inlinePresentation) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ if (mHeader != null || mFooter != null) {
+ throw new IllegalStateException("Already called #setHeader() or #setFooter()");
+ }
+
+ if (authentication == null ^ (presentation == null && inlinePresentation == null)) {
+ throw new IllegalArgumentException("authentication and presentation "
+ + "(dropdown or inline), must be both non-null or null");
+ }
+ mAuthentication = authentication;
+ mPresentation = presentation;
+ mInlinePresentation = inlinePresentation;
+ mAuthenticationIds = assertValid(ids);
+ return this;
+ }
+
+ /**
+ * Specifies views that should not trigger new
+ * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
+ * FillCallback)} requests.
+ *
+ * <p>This is typically used when the service cannot autofill the view; for example, a
+ * text field representing the result of a Captcha challenge.
+ */
+ @NonNull
+ public Builder setIgnoredIds(AutofillId...ids) {
+ throwIfDestroyed();
+ mIgnoredIds = ids;
+ return this;
+ }
+
+ /**
+ * Adds a new {@link Dataset} to this response.
+ *
+ * <p><b>Note: </b> on Android {@link android.os.Build.VERSION_CODES#O}, the total number of
+ * datasets is limited by the Binder transaction size, so it's recommended to keep it
+ * small (in the range of 10-20 at most) and use pagination by adding a fake
+ * {@link Dataset.Builder#setAuthentication(IntentSender) authenticated dataset} at the end
+ * with a presentation string like "Next 10" that would return a new {@link FillResponse}
+ * with the next 10 datasets, and so on. This limitation was lifted on
+ * Android {@link android.os.Build.VERSION_CODES#O_MR1}, although the Binder transaction
+ * size can still be reached if each dataset itself is too big.
+ *
+ * @return This builder.
+ */
+ @NonNull
+ public Builder addDataset(@Nullable Dataset dataset) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ if (dataset == null) {
+ return this;
+ }
+ if (mDatasets == null) {
+ mDatasets = new ArrayList<>();
+ }
+ if (!mDatasets.add(dataset)) {
+ return this;
+ }
+ return this;
+ }
+
+ /**
+ * Sets the {@link SaveInfo} associated with this response.
+ *
+ * @return This builder.
+ */
+ public @NonNull Builder setSaveInfo(@NonNull SaveInfo saveInfo) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ mSaveInfo = saveInfo;
+ return this;
+ }
+
+ /**
+ * Sets a bundle with state that is passed to subsequent APIs that manipulate this response.
+ *
+ * <p>You can use this bundle to store intermediate state that is passed to subsequent calls
+ * to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal,
+ * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}, and
+ * you can also retrieve it by calling {@link FillEventHistory.Event#getClientState()}.
+ *
+ * <p>If this method is called on multiple {@link FillResponse} objects for the same
+ * screen, just the latest bundle is passed back to the service.
+ *
+ * @param clientState The custom client state.
+ * @return This builder.
+ */
+ @NonNull
+ public Builder setClientState(@Nullable Bundle clientState) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ mClientState = clientState;
+ return this;
+ }
+
+ /**
+ * Sets which fields are used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>
+ *
+ * <p><b>Note:</b> This method automatically adds the
+ * {@link FillResponse#FLAG_TRACK_CONTEXT_COMMITED} to the {@link #setFlags(int) flags}.
+
+ * @throws IllegalArgumentException is length of {@code ids} args is more than
+ * {@link UserData#getMaxFieldClassificationIdsSize()}.
+ * @throws IllegalStateException if {@link #build()} or {@link #disableAutofill(long)} was
+ * already called.
+ * @throws NullPointerException if {@code ids} or any element on it is {@code null}.
+ */
+ @NonNull
+ public Builder setFieldClassificationIds(@NonNull AutofillId... ids) {
+ throwIfDestroyed();
+ throwIfDisableAutofillCalled();
+ Preconditions.checkArrayElementsNotNull(ids, "ids");
+ Preconditions.checkArgumentInRange(ids.length, 1,
+ UserData.getMaxFieldClassificationIdsSize(), "ids length");
+ mFieldClassificationIds = ids;
+ mFlags |= FLAG_TRACK_CONTEXT_COMMITED;
+ return this;
+ }
+
+ /**
+ * Sets flags changing the response behavior.
+ *
+ * @param flags a combination of {@link #FLAG_TRACK_CONTEXT_COMMITED} and
+ * {@link #FLAG_DISABLE_ACTIVITY_ONLY}, or {@code 0}.
+ *
+ * @return This builder.
+ */
+ @NonNull
+ public Builder setFlags(@FillResponseFlags int flags) {
+ throwIfDestroyed();
+ mFlags = Preconditions.checkFlagsArgument(flags,
+ FLAG_TRACK_CONTEXT_COMMITED | FLAG_DISABLE_ACTIVITY_ONLY);
+ return this;
+ }
+
+ /**
+ * Disables autofill for the app or activity.
+ *
+ * <p>This method is useful to optimize performance in cases where the service knows it
+ * can not autofill an app—for example, when the service has a list of "blacklisted"
+ * apps such as office suites.
+ *
+ * <p>By default, it disables autofill for all activities in the app, unless the response is
+ * {@link #setFlags(int) flagged} with {@link #FLAG_DISABLE_ACTIVITY_ONLY}.
+ *
+ * <p>Autofill for the app or activity is automatically re-enabled after any of the
+ * following conditions:
+ *
+ * <ol>
+ * <li>{@code duration} milliseconds have passed.
+ * <li>The autofill service for the user has changed.
+ * <li>The device has rebooted.
+ * </ol>
+ *
+ * <p><b>Note:</b> Activities that are running when autofill is re-enabled remain
+ * disabled for autofill until they finish and restart.
+ *
+ * @param duration duration to disable autofill, in milliseconds.
+ *
+ * @return this builder
+ *
+ * @throws IllegalArgumentException if {@code duration} is not a positive number.
+ * @throws IllegalStateException if either {@link #addDataset(Dataset)},
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)},
+ * {@link #setSaveInfo(SaveInfo)}, {@link #setClientState(Bundle)}, or
+ * {@link #setFieldClassificationIds(AutofillId...)} was already called.
+ */
+ @NonNull
+ public Builder disableAutofill(long duration) {
+ throwIfDestroyed();
+ if (duration <= 0) {
+ throw new IllegalArgumentException("duration must be greater than 0");
+ }
+ if (mAuthentication != null || mDatasets != null || mSaveInfo != null
+ || mFieldClassificationIds != null || mClientState != null) {
+ throw new IllegalStateException("disableAutofill() must be the only method called");
+ }
+
+ mDisableDuration = duration;
+ return this;
+ }
+
+ /**
+ * Sets a header to be shown as the first element in the list of datasets.
+ *
+ * <p>When this method is called, you must also {@link #addDataset(Dataset) add a dataset},
+ * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this
+ * method should only be used on {@link FillResponse FillResponses} that do not require
+ * authentication (as the header could have been set directly in the main presentation in
+ * these cases).
+ *
+ * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
+ * or background color: Autofill on different platforms may have different themes.
+ *
+ * @param header a presentation to represent the header. This presentation is not clickable
+ * —calling
+ * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would
+ * have no effect.
+ *
+ * @return this builder
+ *
+ * @throws IllegalStateException if an
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) authentication} was
+ * already set for this builder.
+ */
+ // TODO(b/69796626): make it sticky / update javadoc
+ @NonNull
+ public Builder setHeader(@NonNull RemoteViews header) {
+ throwIfDestroyed();
+ throwIfAuthenticationCalled();
+ mHeader = Preconditions.checkNotNull(header);
+ return this;
+ }
+
+ /**
+ * Sets a footer to be shown as the last element in the list of datasets.
+ *
+ * <p>When this method is called, you must also {@link #addDataset(Dataset) add a dataset},
+ * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this
+ * method should only be used on {@link FillResponse FillResponses} that do not require
+ * authentication (as the footer could have been set directly in the main presentation in
+ * these cases).
+ *
+ * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
+ * or background color: Autofill on different platforms may have different themes.
+ *
+ * @param footer a presentation to represent the footer. This presentation is not clickable
+ * —calling
+ * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would
+ * have no effect.
+ *
+ * @return this builder
+ *
+ * @throws IllegalStateException if the FillResponse
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)
+ * requires authentication}.
+ */
+ // TODO(b/69796626): make it sticky / update javadoc
+ @NonNull
+ public Builder setFooter(@NonNull RemoteViews footer) {
+ throwIfDestroyed();
+ throwIfAuthenticationCalled();
+ mFooter = Preconditions.checkNotNull(footer);
+ return this;
+ }
+
+ /**
+ * Sets a specific {@link UserData} for field classification for this request only.
+ *
+ * <p>Any fields in this UserData will override corresponding fields in the generic
+ * UserData object
+ *
+ * @return this builder
+ * @throws IllegalStateException if the FillResponse
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)
+ * requires authentication}.
+ */
+ @NonNull
+ public Builder setUserData(@NonNull UserData userData) {
+ throwIfDestroyed();
+ throwIfAuthenticationCalled();
+ mUserData = Preconditions.checkNotNull(userData);
+ return this;
+ }
+
+ /**
+ * Sets target resource IDs of the child view in {@link RemoteViews Presentation Template}
+ * which will cancel the session when clicked.
+ * Those targets will be respectively applied to a child of the header, footer and
+ * each {@link Dataset}.
+ *
+ * @param ids array of the resource id. Empty list or non-existing id has no effect.
+ *
+ * @return this builder
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ */
+ @NonNull
+ public Builder setPresentationCancelIds(@Nullable int[] ids) {
+ throwIfDestroyed();
+ mCancelIds = ids;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link FillResponse} instance.
+ *
+ * @throws IllegalStateException if any of the following conditions occur:
+ * <ol>
+ * <li>{@link #build()} was already called.
+ * <li>No call was made to {@link #addDataset(Dataset)},
+ * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)},
+ * {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)},
+ * {@link #setClientState(Bundle)},
+ * or {@link #setFieldClassificationIds(AutofillId...)}.
+ * <li>{@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} is called
+ * without any previous calls to {@link #addDataset(Dataset)}.
+ * </ol>
+ *
+ * @return A built response.
+ */
+ @NonNull
+ public FillResponse build() {
+ throwIfDestroyed();
+ if (mAuthentication == null && mDatasets == null && mSaveInfo == null
+ && mDisableDuration == 0 && mFieldClassificationIds == null
+ && mClientState == null) {
+ throw new IllegalStateException("need to provide: at least one DataSet, or a "
+ + "SaveInfo, or an authentication with a presentation, "
+ + "or a FieldsDetection, or a client state, or disable autofill");
+ }
+ if (mDatasets == null && (mHeader != null || mFooter != null)) {
+ throw new IllegalStateException(
+ "must add at least 1 dataset when using header or footer");
+ }
+
+ if (mDatasets != null) {
+ for (final Dataset dataset : mDatasets) {
+ if (dataset.getFieldInlinePresentation(0) != null) {
+ mSupportsInlineSuggestions = true;
+ break;
+ }
+ }
+ } else if (mInlinePresentation != null) {
+ mSupportsInlineSuggestions = true;
+ }
+
+ mDestroyed = true;
+ return new FillResponse(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+
+ private void throwIfDisableAutofillCalled() {
+ if (mDisableDuration > 0) {
+ throw new IllegalStateException("Already called #disableAutofill()");
+ }
+ }
+
+ private void throwIfAuthenticationCalled() {
+ if (mAuthentication != null) {
+ throw new IllegalStateException("Already called #setAuthentication()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ // TODO: create a dump() method instead
+ final StringBuilder builder = new StringBuilder(
+ "FillResponse : [mRequestId=" + mRequestId);
+ if (mDatasets != null) {
+ builder.append(", datasets=").append(mDatasets.getList());
+ }
+ if (mSaveInfo != null) {
+ builder.append(", saveInfo=").append(mSaveInfo);
+ }
+ if (mClientState != null) {
+ builder.append(", hasClientState");
+ }
+ if (mPresentation != null) {
+ builder.append(", hasPresentation");
+ }
+ if (mInlinePresentation != null) {
+ builder.append(", hasInlinePresentation");
+ }
+ if (mHeader != null) {
+ builder.append(", hasHeader");
+ }
+ if (mFooter != null) {
+ builder.append(", hasFooter");
+ }
+ if (mAuthentication != null) {
+ builder.append(", hasAuthentication");
+ }
+ if (mAuthenticationIds != null) {
+ builder.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds));
+ }
+ builder.append(", disableDuration=").append(mDisableDuration);
+ if (mFlags != 0) {
+ builder.append(", flags=").append(mFlags);
+ }
+ if (mFieldClassificationIds != null) {
+ builder.append(Arrays.toString(mFieldClassificationIds));
+ }
+ if (mUserData != null) {
+ builder.append(", userData=").append(mUserData);
+ }
+ if (mCancelIds != null) {
+ builder.append(", mCancelIds=").append(mCancelIds.length);
+ }
+ builder.append(", mSupportInlinePresentations=").append(mSupportsInlineSuggestions);
+ return builder.append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mDatasets, flags);
+ parcel.writeParcelable(mSaveInfo, flags);
+ parcel.writeParcelable(mClientState, flags);
+ parcel.writeParcelableArray(mAuthenticationIds, flags);
+ parcel.writeParcelable(mAuthentication, flags);
+ parcel.writeParcelable(mPresentation, flags);
+ parcel.writeParcelable(mInlinePresentation, flags);
+ parcel.writeParcelable(mHeader, flags);
+ parcel.writeParcelable(mFooter, flags);
+ parcel.writeParcelable(mUserData, flags);
+ parcel.writeParcelableArray(mIgnoredIds, flags);
+ parcel.writeLong(mDisableDuration);
+ parcel.writeParcelableArray(mFieldClassificationIds, flags);
+ parcel.writeInt(mFlags);
+ parcel.writeIntArray(mCancelIds);
+ parcel.writeInt(mRequestId);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<FillResponse> CREATOR =
+ new Parcelable.Creator<FillResponse>() {
+ @Override
+ public FillResponse createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final Builder builder = new Builder();
+ final ParceledListSlice<Dataset> datasetSlice = parcel.readParcelable(null);
+ final List<Dataset> datasets = (datasetSlice != null) ? datasetSlice.getList() : null;
+ final int datasetCount = (datasets != null) ? datasets.size() : 0;
+ for (int i = 0; i < datasetCount; i++) {
+ builder.addDataset(datasets.get(i));
+ }
+ builder.setSaveInfo(parcel.readParcelable(null));
+ builder.setClientState(parcel.readParcelable(null));
+
+ // Sets authentication state.
+ final AutofillId[] authenticationIds = parcel.readParcelableArray(null,
+ AutofillId.class);
+ final IntentSender authentication = parcel.readParcelable(null);
+ final RemoteViews presentation = parcel.readParcelable(null);
+ final InlinePresentation inlinePresentation = parcel.readParcelable(null);
+ if (authenticationIds != null) {
+ builder.setAuthentication(authenticationIds, authentication, presentation,
+ inlinePresentation);
+ }
+ final RemoteViews header = parcel.readParcelable(null);
+ if (header != null) {
+ builder.setHeader(header);
+ }
+ final RemoteViews footer = parcel.readParcelable(null);
+ if (footer != null) {
+ builder.setFooter(footer);
+ }
+ final UserData userData = parcel.readParcelable(null);
+ if (userData != null) {
+ builder.setUserData(userData);
+ }
+
+ builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class));
+ final long disableDuration = parcel.readLong();
+ if (disableDuration > 0) {
+ builder.disableAutofill(disableDuration);
+ }
+ final AutofillId[] fieldClassifactionIds =
+ parcel.readParcelableArray(null, AutofillId.class);
+ if (fieldClassifactionIds != null) {
+ builder.setFieldClassificationIds(fieldClassifactionIds);
+ }
+ builder.setFlags(parcel.readInt());
+ final int[] cancelIds = parcel.createIntArray();
+ builder.setPresentationCancelIds(cancelIds);
+
+ final FillResponse response = builder.build();
+ response.setRequestId(parcel.readInt());
+
+ return response;
+ }
+
+ @Override
+ public FillResponse[] newArray(int size) {
+ return new FillResponse[size];
+ }
+ };
+}
diff --git a/android/service/autofill/ImageTransformation.java b/android/service/autofill/ImageTransformation.java
new file mode 100644
index 0000000..974f0ea
--- /dev/null
+++ b/android/service/autofill/ImageTransformation.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+/**
+ * Replaces the content of a child {@link ImageView} of a
+ * {@link RemoteViews presentation template} with the first image that matches a regular expression
+ * (regex).
+ *
+ * <p>Typically used to display credit card logos. Example:
+ *
+ * <pre class="prettyprint">
+ * new ImageTransformation.Builder(ccNumberId, Pattern.compile("^4815.*$"),
+ * R.drawable.ic_credit_card_logo1, "Brand 1")
+ * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2, "Brand 2")
+ * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3, "Brand 3")
+ * .build();
+ * </pre>
+ *
+ * <p>There is no imposed limit in the number of options, but keep in mind that regexs are
+ * expensive to evaluate, so use the minimum number of regexs and add the most common first
+ * (for example, if this is a tranformation for a credit card logo and the most common credit card
+ * issuers are banks X and Y, add the regexes that resolves these 2 banks first).
+ */
+public final class ImageTransformation extends InternalTransformation implements Transformation,
+ Parcelable {
+ private static final String TAG = "ImageTransformation";
+
+ private final AutofillId mId;
+ private final ArrayList<Option> mOptions;
+
+ private ImageTransformation(Builder builder) {
+ mId = builder.mId;
+ mOptions = builder.mOptions;
+ }
+
+ /** @hide */
+ @TestApi
+ @Override
+ public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
+ int childViewId) throws Exception {
+ final String value = finder.findByAutofillId(mId);
+ if (value == null) {
+ Log.w(TAG, "No view for id " + mId);
+ return;
+ }
+ final int size = mOptions.size();
+ if (sDebug) {
+ Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against");
+ }
+
+ for (int i = 0; i < size; i++) {
+ final Option option = mOptions.get(i);
+ try {
+ if (option.pattern.matcher(value).matches()) {
+ Log.d(TAG, "Found match at " + i + ": " + option);
+ parentTemplate.setImageViewResource(childViewId, option.resId);
+ if (option.contentDescription != null) {
+ parentTemplate.setContentDescription(childViewId,
+ option.contentDescription);
+ }
+ return;
+ }
+ } catch (Exception e) {
+ // Do not log full exception to avoid PII leaking
+ Log.w(TAG, "Error matching regex #" + i + "(" + option.pattern + ") on id "
+ + option.resId + ": " + e.getClass());
+ throw e;
+
+ }
+ }
+ if (sDebug) Log.d(TAG, "No match for " + value);
+ }
+
+ /**
+ * Builder for {@link ImageTransformation} objects.
+ */
+ public static class Builder {
+ private final AutofillId mId;
+ private final ArrayList<Option> mOptions = new ArrayList<>();
+ private boolean mDestroyed;
+
+ /**
+ * Creates a new builder for a autofill id and add a first option.
+ *
+ * @param id id of the screen field that will be used to evaluate whether the image should
+ * be used.
+ * @param regex regular expression defining what should be matched to use this image.
+ * @param resId resource id of the image (in the autofill service's package). The
+ * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
+ *
+ * @deprecated use
+ * {@link #Builder(AutofillId, Pattern, int, CharSequence)} instead.
+ */
+ @Deprecated
+ public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId) {
+ mId = Preconditions.checkNotNull(id);
+ addOption(regex, resId);
+ }
+
+ /**
+ * Creates a new builder for a autofill id and add a first option.
+ *
+ * @param id id of the screen field that will be used to evaluate whether the image should
+ * be used.
+ * @param regex regular expression defining what should be matched to use this image.
+ * @param resId resource id of the image (in the autofill service's package). The
+ * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
+ * @param contentDescription content description to be applied in the child view.
+ */
+ public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId,
+ @NonNull CharSequence contentDescription) {
+ mId = Preconditions.checkNotNull(id);
+ addOption(regex, resId, contentDescription);
+ }
+
+ /**
+ * Adds an option to replace the child view with a different image when the regex matches.
+ *
+ * @param regex regular expression defining what should be matched to use this image.
+ * @param resId resource id of the image (in the autofill service's package). The
+ * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
+ *
+ * @return this build
+ *
+ * @deprecated use {@link #addOption(Pattern, int, CharSequence)} instead.
+ */
+ @Deprecated
+ public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId) {
+ addOptionInternal(regex, resId, null);
+ return this;
+ }
+
+ /**
+ * Adds an option to replace the child view with a different image and content description
+ * when the regex matches.
+ *
+ * @param regex regular expression defining what should be matched to use this image.
+ * @param resId resource id of the image (in the autofill service's package). The
+ * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
+ * @param contentDescription content description to be applied in the child view.
+ *
+ * @return this build
+ */
+ public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId,
+ @NonNull CharSequence contentDescription) {
+ addOptionInternal(regex, resId, Preconditions.checkNotNull(contentDescription));
+ return this;
+ }
+
+ private void addOptionInternal(@NonNull Pattern regex, @DrawableRes int resId,
+ @Nullable CharSequence contentDescription) {
+ throwIfDestroyed();
+
+ Preconditions.checkNotNull(regex);
+ Preconditions.checkArgument(resId != 0);
+
+ mOptions.add(new Option(regex, resId, contentDescription));
+ }
+
+
+ /**
+ * Creates a new {@link ImageTransformation} instance.
+ */
+ public ImageTransformation build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new ImageTransformation(this);
+ }
+
+ private void throwIfDestroyed() {
+ Preconditions.checkState(!mDestroyed, "Already called build()");
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "ImageTransformation: [id=" + mId + ", options=" + mOptions + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mId, flags);
+
+ final int size = mOptions.size();
+ final Pattern[] patterns = new Pattern[size];
+ final int[] resIds = new int[size];
+ final CharSequence[] contentDescriptions = new String[size];
+ for (int i = 0; i < size; i++) {
+ final Option option = mOptions.get(i);
+ patterns[i] = option.pattern;
+ resIds[i] = option.resId;
+ contentDescriptions[i] = option.contentDescription;
+ }
+ parcel.writeSerializable(patterns);
+ parcel.writeIntArray(resIds);
+ parcel.writeCharSequenceArray(contentDescriptions);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ImageTransformation> CREATOR =
+ new Parcelable.Creator<ImageTransformation>() {
+ @Override
+ public ImageTransformation createFromParcel(Parcel parcel) {
+ final AutofillId id = parcel.readParcelable(null);
+
+ final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
+ final int[] resIds = parcel.createIntArray();
+ final CharSequence[] contentDescriptions = parcel.readCharSequenceArray();
+
+ // Always go through the builder to ensure the data ingested by the system obeys the
+ // contract of the builder to avoid attacks using specially crafted parcels.
+ final CharSequence contentDescription = contentDescriptions[0];
+ final ImageTransformation.Builder builder = (contentDescription != null)
+ ? new ImageTransformation.Builder(id, regexs[0], resIds[0], contentDescription)
+ : new ImageTransformation.Builder(id, regexs[0], resIds[0]);
+
+ final int size = regexs.length;
+ for (int i = 1; i < size; i++) {
+ if (contentDescriptions[i] != null) {
+ builder.addOption(regexs[i], resIds[i], contentDescriptions[i]);
+ } else {
+ builder.addOption(regexs[i], resIds[i]);
+ }
+ }
+
+ return builder.build();
+ }
+
+ @Override
+ public ImageTransformation[] newArray(int size) {
+ return new ImageTransformation[size];
+ }
+ };
+
+ private static final class Option {
+ public final Pattern pattern;
+ public final int resId;
+ public final CharSequence contentDescription;
+
+ Option(Pattern pattern, int resId, CharSequence contentDescription) {
+ this.pattern = pattern;
+ this.resId = resId;
+ this.contentDescription = TextUtils.trimNoCopySpans(contentDescription);
+ }
+ }
+}
diff --git a/android/service/autofill/InlinePresentation.java b/android/service/autofill/InlinePresentation.java
new file mode 100644
index 0000000..9cf1b87
--- /dev/null
+++ b/android/service/autofill/InlinePresentation.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2019 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.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Size;
+import android.app.slice.Slice;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.widget.inline.InlinePresentationSpec;
+
+import com.android.internal.util.DataClass;
+
+import java.util.List;
+
+/**
+ * Wrapper class holding a {@link Slice} and an {@link InlinePresentationSpec} for rendering UI
+ * for an Inline Suggestion.
+ */
+@DataClass(
+ genToString = true,
+ genHiddenConstDefs = true,
+ genEqualsHashCode = true)
+public final class InlinePresentation implements Parcelable {
+
+
+ /**
+ * Represents the UI content and the action for the inline suggestion.
+ */
+ private final @NonNull Slice mSlice;
+
+ /**
+ * Specifies the UI specification for the inline suggestion.
+ */
+ private final @NonNull InlinePresentationSpec mInlinePresentationSpec;
+
+ /**
+ * Indicates whether the UI should be pinned, hence non-scrollable and non-filterable, in the
+ * host.
+ */
+ private final boolean mPinned;
+
+ /**
+ * Returns the autofill hints set in the slice.
+ *
+ * @hide
+ */
+ @NonNull
+ @Size(min = 0)
+ public String[] getAutofillHints() {
+ List<String> hints = mSlice.getHints();
+ return hints.toArray(new String[hints.size()]);
+ }
+
+
+
+ // Code below generated by codegen v1.0.15.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/InlinePresentation.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new InlinePresentation.
+ *
+ * @param slice
+ * Represents the UI content and the action for the inline suggestion.
+ * @param inlinePresentationSpec
+ * Specifies the UI specification for the inline suggestion.
+ * @param pinned
+ * Indicates whether the UI should be pinned, hence non-scrollable and non-filterable, in the
+ * host.
+ */
+ @DataClass.Generated.Member
+ public InlinePresentation(
+ @NonNull Slice slice,
+ @NonNull InlinePresentationSpec inlinePresentationSpec,
+ boolean pinned) {
+ this.mSlice = slice;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSlice);
+ this.mInlinePresentationSpec = inlinePresentationSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlinePresentationSpec);
+ this.mPinned = pinned;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * Represents the UI content and the action for the inline suggestion.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Slice getSlice() {
+ return mSlice;
+ }
+
+ /**
+ * Specifies the UI specification for the inline suggestion.
+ */
+ @DataClass.Generated.Member
+ public @NonNull InlinePresentationSpec getInlinePresentationSpec() {
+ return mInlinePresentationSpec;
+ }
+
+ /**
+ * Indicates whether the UI should be pinned, hence non-scrollable and non-filterable, in the
+ * host.
+ */
+ @DataClass.Generated.Member
+ public boolean isPinned() {
+ return mPinned;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "InlinePresentation { " +
+ "slice = " + mSlice + ", " +
+ "inlinePresentationSpec = " + mInlinePresentationSpec + ", " +
+ "pinned = " + mPinned +
+ " }";
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public boolean equals(@android.annotation.Nullable Object o) {
+ // You can override field equality logic by defining either of the methods like:
+ // boolean fieldNameEquals(InlinePresentation other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ InlinePresentation that = (InlinePresentation) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && java.util.Objects.equals(mSlice, that.mSlice)
+ && java.util.Objects.equals(mInlinePresentationSpec, that.mInlinePresentationSpec)
+ && mPinned == that.mPinned;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int hashCode() {
+ // You can override field hashCode logic by defining methods like:
+ // int fieldNameHashCode() { ... }
+
+ int _hash = 1;
+ _hash = 31 * _hash + java.util.Objects.hashCode(mSlice);
+ _hash = 31 * _hash + java.util.Objects.hashCode(mInlinePresentationSpec);
+ _hash = 31 * _hash + Boolean.hashCode(mPinned);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mPinned) flg |= 0x4;
+ dest.writeByte(flg);
+ dest.writeTypedObject(mSlice, flags);
+ dest.writeTypedObject(mInlinePresentationSpec, flags);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ InlinePresentation(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ boolean pinned = (flg & 0x4) != 0;
+ Slice slice = (Slice) in.readTypedObject(Slice.CREATOR);
+ InlinePresentationSpec inlinePresentationSpec = (InlinePresentationSpec) in.readTypedObject(InlinePresentationSpec.CREATOR);
+
+ this.mSlice = slice;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mSlice);
+ this.mInlinePresentationSpec = inlinePresentationSpec;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mInlinePresentationSpec);
+ this.mPinned = pinned;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<InlinePresentation> CREATOR
+ = new Parcelable.Creator<InlinePresentation>() {
+ @Override
+ public InlinePresentation[] newArray(int size) {
+ return new InlinePresentation[size];
+ }
+
+ @Override
+ public InlinePresentation createFromParcel(@NonNull Parcel in) {
+ return new InlinePresentation(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1586992400667L,
+ codegenVersion = "1.0.15",
+ sourceFile = "frameworks/base/core/java/android/service/autofill/InlinePresentation.java",
+ inputSignatures = "private final @android.annotation.NonNull android.app.slice.Slice mSlice\nprivate final @android.annotation.NonNull android.widget.inline.InlinePresentationSpec mInlinePresentationSpec\nprivate final boolean mPinned\npublic @android.annotation.NonNull @android.annotation.Size(min=0L) java.lang.String[] getAutofillHints()\nclass InlinePresentation extends java.lang.Object implements [android.os.Parcelable]\[email protected](genToString=true, genHiddenConstDefs=true, genEqualsHashCode=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/service/autofill/InlineSuggestionRenderService.java b/android/service/autofill/InlineSuggestionRenderService.java
new file mode 100644
index 0000000..6c22b19
--- /dev/null
+++ b/android/service/autofill/InlineSuggestionRenderService.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2020 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.autofill;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.graphics.PixelFormat;
+import android.os.BaseBundle;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Size;
+import android.view.Display;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+/**
+ * A service that renders an inline presentation view given the {@link InlinePresentation}.
+ *
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public abstract class InlineSuggestionRenderService extends Service {
+
+ private static final String TAG = "InlineSuggestionRenderService";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ *
+ * <p>To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_INLINE_SUGGESTION_RENDER_SERVICE} permission so that
+ * other applications can not abuse it.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.autofill.InlineSuggestionRenderService";
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
+
+ private IInlineSuggestionUiCallback mCallback;
+
+ /**
+ * If the specified {@code width}/{@code height} is an exact value, then it will be returned as
+ * is, otherwise the method tries to measure a size that is just large enough to fit the view
+ * content, within constraints posed by {@code minSize} and {@code maxSize}.
+ *
+ * @param view the view for which we measure the size
+ * @param width the expected width of the view, either an exact value or {@link
+ * ViewGroup.LayoutParams#WRAP_CONTENT}
+ * @param height the expected width of the view, either an exact value or {@link
+ * ViewGroup.LayoutParams#WRAP_CONTENT}
+ * @param minSize the lower bound of the size to be returned
+ * @param maxSize the upper bound of the size to be returned
+ * @return the measured size of the view based on the given size constraints.
+ */
+ private Size measuredSize(@NonNull View view, int width, int height, @NonNull Size minSize,
+ @NonNull Size maxSize) {
+ if (width != ViewGroup.LayoutParams.WRAP_CONTENT
+ && height != ViewGroup.LayoutParams.WRAP_CONTENT) {
+ return new Size(width, height);
+ }
+ int widthMeasureSpec;
+ if (width == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxSize.getWidth(),
+ View.MeasureSpec.AT_MOST);
+ } else {
+ widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
+ }
+ int heightMeasureSpec;
+ if (height == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxSize.getHeight(),
+ View.MeasureSpec.AT_MOST);
+ } else {
+ heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
+ }
+ view.measure(widthMeasureSpec, heightMeasureSpec);
+ return new Size(Math.max(view.getMeasuredWidth(), minSize.getWidth()),
+ Math.max(view.getMeasuredHeight(), minSize.getHeight()));
+ }
+
+ private void handleRenderSuggestion(IInlineSuggestionUiCallback callback,
+ InlinePresentation presentation, int width, int height, IBinder hostInputToken,
+ int displayId) {
+ if (hostInputToken == null) {
+ try {
+ callback.onError();
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling onError()");
+ }
+ return;
+ }
+
+ // When we create the UI it should be for the IME display
+ updateDisplay(displayId);
+ try {
+ final View suggestionView = onRenderSuggestion(presentation, width, height);
+ if (suggestionView == null) {
+ Log.w(TAG, "ExtServices failed to render the inline suggestion view.");
+ try {
+ callback.onError();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Null suggestion view returned by renderer");
+ }
+ return;
+ }
+ mCallback = callback;
+ final Size measuredSize = measuredSize(suggestionView, width, height,
+ presentation.getInlinePresentationSpec().getMinSize(),
+ presentation.getInlinePresentationSpec().getMaxSize());
+ Log.v(TAG, "width=" + width + ", height=" + height + ", measuredSize=" + measuredSize);
+
+ final InlineSuggestionRoot suggestionRoot = new InlineSuggestionRoot(this, callback);
+ suggestionRoot.addView(suggestionView);
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams(measuredSize.getWidth(),
+ measuredSize.getHeight(), WindowManager.LayoutParams.TYPE_APPLICATION, 0,
+ PixelFormat.TRANSPARENT);
+
+ final SurfaceControlViewHost host = new SurfaceControlViewHost(this, getDisplay(),
+ hostInputToken);
+ host.setView(suggestionRoot, lp);
+
+ // Set the suggestion view to be non-focusable so that if its background is set to a
+ // ripple drawable, the ripple won't be shown initially.
+ suggestionView.setFocusable(false);
+ suggestionView.setOnClickListener((v) -> {
+ try {
+ callback.onClick();
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling onClick()");
+ }
+ });
+ final View.OnLongClickListener onLongClickListener =
+ suggestionView.getOnLongClickListener();
+ suggestionView.setOnLongClickListener((v) -> {
+ if (onLongClickListener != null) {
+ onLongClickListener.onLongClick(v);
+ }
+ try {
+ callback.onLongClick();
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling onLongClick()");
+ }
+ return true;
+ });
+
+ sendResult(callback, host.getSurfacePackage(), measuredSize.getWidth(),
+ measuredSize.getHeight());
+ } finally {
+ updateDisplay(Display.DEFAULT_DISPLAY);
+ }
+ }
+
+ private void handleGetInlineSuggestionsRendererInfo(@NonNull RemoteCallback callback) {
+ final Bundle rendererInfo = onGetInlineSuggestionsRendererInfo();
+ callback.sendResult(rendererInfo);
+ }
+
+ private void sendResult(@NonNull IInlineSuggestionUiCallback callback,
+ @Nullable SurfaceControlViewHost.SurfacePackage surface, int width, int height) {
+ try {
+ callback.onContent(surface, width, height);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling onContent(" + surface + ")");
+ }
+ }
+
+ @Override
+ @Nullable
+ public final IBinder onBind(@NonNull Intent intent) {
+ BaseBundle.setShouldDefuse(true);
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return new IInlineSuggestionRenderService.Stub() {
+ @Override
+ public void renderSuggestion(@NonNull IInlineSuggestionUiCallback callback,
+ @NonNull InlinePresentation presentation, int width, int height,
+ @Nullable IBinder hostInputToken, int displayId) {
+ mHandler.sendMessage(
+ obtainMessage(InlineSuggestionRenderService::handleRenderSuggestion,
+ InlineSuggestionRenderService.this, callback, presentation,
+ width, height, hostInputToken, displayId));
+ }
+
+ @Override
+ public void getInlineSuggestionsRendererInfo(@NonNull RemoteCallback callback) {
+ mHandler.sendMessage(obtainMessage(
+ InlineSuggestionRenderService::handleGetInlineSuggestionsRendererInfo,
+ InlineSuggestionRenderService.this, callback));
+ }
+ }.asBinder();
+ }
+
+ Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+ return null;
+ }
+
+ /**
+ * Starts the {@link IntentSender} from the client app.
+ *
+ * @param intentSender the {@link IntentSender} to start the attribution UI from the client
+ * app.
+ */
+ public final void startIntentSender(@NonNull IntentSender intentSender) {
+ if (mCallback == null) return;
+ try {
+ mCallback.onStartIntentSender(intentSender);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the metadata about the renderer. Returns {@code Bundle.Empty} if no metadata is
+ * provided.
+ */
+ @NonNull
+ public Bundle onGetInlineSuggestionsRendererInfo() {
+ return Bundle.EMPTY;
+ }
+
+ /**
+ * Renders the slice into a view.
+ */
+ @Nullable
+ public View onRenderSuggestion(@NonNull InlinePresentation presentation, int width,
+ int height) {
+ Log.e(TAG, "service implementation (" + getClass() + " does not implement "
+ + "onRenderSuggestion()");
+ return null;
+ }
+}
diff --git a/android/service/autofill/InlineSuggestionRoot.java b/android/service/autofill/InlineSuggestionRoot.java
new file mode 100644
index 0000000..c879653
--- /dev/null
+++ b/android/service/autofill/InlineSuggestionRoot.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 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.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.widget.FrameLayout;
+
+/**
+ * This class is the root view for an inline suggestion. It is responsible for
+ * detecting the click on the item and to also transfer input focus to the IME
+ * window if we detect the user is scrolling.
+ *
+ * @hide
+ */
+@SuppressLint("ViewConstructor")
+public class InlineSuggestionRoot extends FrameLayout {
+ private static final String TAG = "InlineSuggestionRoot";
+
+ private final @NonNull IInlineSuggestionUiCallback mCallback;
+ private final int mTouchSlop;
+
+ private float mDownX;
+ private float mDownY;
+
+ public InlineSuggestionRoot(@NonNull Context context,
+ @NonNull IInlineSuggestionUiCallback callback) {
+ super(context);
+ mCallback = callback;
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ setFocusable(false);
+ }
+
+ @Override
+ @SuppressLint("ClickableViewAccessibility")
+ public boolean dispatchTouchEvent(@NonNull MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ mDownX = event.getX();
+ mDownY = event.getY();
+ } break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final float distance = MathUtils.dist(mDownX, mDownY,
+ event.getX(), event.getY());
+ final boolean isSecure = (event.getFlags()
+ & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) == 0;
+ if (!isSecure || distance > mTouchSlop) {
+ try {
+ mCallback.onTransferTouchFocusToImeWindow(getViewRootImpl().getInputToken(),
+ getContext().getDisplayId());
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException transferring touch focus to IME");
+ }
+ }
+ } break;
+ }
+ return super.dispatchTouchEvent(event);
+ }
+}
diff --git a/android/service/autofill/InternalOnClickAction.java b/android/service/autofill/InternalOnClickAction.java
new file mode 100644
index 0000000..6602f2d
--- /dev/null
+++ b/android/service/autofill/InternalOnClickAction.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 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.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+import android.view.ViewGroup;
+
+/**
+ * Superclass of all {@link OnClickAction} the system understands. As this is not public, all public
+ * subclasses have to implement {@link OnClickAction} again.
+ *
+ * @hide
+ */
+@TestApi
+public abstract class InternalOnClickAction implements OnClickAction, Parcelable {
+
+ /**
+ * Applies the action to the children of the {@code rootView} when clicked.
+ */
+ public abstract void onClick(@NonNull ViewGroup rootView);
+}
diff --git a/android/service/autofill/InternalSanitizer.java b/android/service/autofill/InternalSanitizer.java
new file mode 100644
index 0000000..ccffc70
--- /dev/null
+++ b/android/service/autofill/InternalSanitizer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+import android.view.autofill.AutofillValue;
+
+/**
+ * Superclass of all sanitizers the system understands. As this is not public all public subclasses
+ * have to implement {@link Sanitizer} again.
+ *
+ * @hide
+ */
+@TestApi
+public abstract class InternalSanitizer implements Sanitizer, Parcelable {
+
+ /**
+ * Sanitizes an {@link AutofillValue}.
+ *
+ * @return sanitized value or {@code null} if value could not be sanitized (for example: didn't
+ * match regex, it's an invalid type, regex failed, etc).
+ */
+ @Nullable
+ public abstract AutofillValue sanitize(@NonNull AutofillValue value);
+}
diff --git a/android/service/autofill/InternalTransformation.java b/android/service/autofill/InternalTransformation.java
new file mode 100644
index 0000000..0dba2b9
--- /dev/null
+++ b/android/service/autofill/InternalTransformation.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+import android.util.Log;
+import android.util.Pair;
+import android.widget.RemoteViews;
+
+import java.util.ArrayList;
+
+/**
+ * Superclass of all transformation the system understands. As this is not public all
+ * subclasses have to implement {@link Transformation} again.
+ *
+ * @hide
+ */
+@TestApi
+public abstract class InternalTransformation implements Transformation, Parcelable {
+
+ private static final String TAG = "InternalTransformation";
+
+ /**
+ * Applies this transformation to a child view of a {@link android.widget.RemoteViews
+ * presentation template}.
+ *
+ * @param finder object used to find the value of a field in the screen.
+ * @param template the {@link RemoteViews presentation template}.
+ * @param childViewId resource id of the child view inside the template.
+ */
+ abstract void apply(@NonNull ValueFinder finder, @NonNull RemoteViews template,
+ int childViewId) throws Exception;
+
+ /**
+ * Applies multiple transformations to the children views of a
+ * {@link android.widget.RemoteViews presentation template}.
+ *
+ * @param finder object used to find the value of a field in the screen.
+ * @param template the {@link RemoteViews presentation template}.
+ * @param transformations map of resource id of the child view inside the template to
+ * transformation.
+ */
+ public static boolean batchApply(@NonNull ValueFinder finder, @NonNull RemoteViews template,
+ @NonNull ArrayList<Pair<Integer, InternalTransformation>> transformations) {
+ final int size = transformations.size();
+ if (sDebug) Log.d(TAG, "getPresentation(): applying " + size + " transformations");
+ for (int i = 0; i < size; i++) {
+ final Pair<Integer, InternalTransformation> pair = transformations.get(i);
+ final int id = pair.first;
+ final InternalTransformation transformation = pair.second;
+ if (sDebug) Log.d(TAG, "#" + i + ": " + transformation);
+
+ try {
+ transformation.apply(finder, template, id);
+ } catch (Exception e) {
+ // Do not log full exception to avoid PII leaking
+ Log.e(TAG, "Could not apply transformation " + transformation + ": "
+ + e.getClass());
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/android/service/autofill/InternalValidator.java b/android/service/autofill/InternalValidator.java
new file mode 100644
index 0000000..4bea98d
--- /dev/null
+++ b/android/service/autofill/InternalValidator.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcelable;
+
+/**
+ * Superclass of all validators the system understands. As this is not public all public subclasses
+ * have to implement {@link Validator} again.
+ *
+ * @hide
+ */
+@TestApi
+public abstract class InternalValidator implements Validator, Parcelable {
+
+ /**
+ * Decides whether the contents of the screen are valid.
+ *
+ * @param finder object used to find the value of a field in the screen.
+ * @return {@code true} if the contents are valid, {@code false} otherwise.
+ */
+ public abstract boolean isValid(@NonNull ValueFinder finder);
+}
diff --git a/android/service/autofill/LuhnChecksumValidator.java b/android/service/autofill/LuhnChecksumValidator.java
new file mode 100644
index 0000000..ef0bd74
--- /dev/null
+++ b/android/service/autofill/LuhnChecksumValidator.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Arrays;
+
+/**
+ * Validator that returns {@code true} if the number created by concatenating all given fields
+ * pass a Luhn algorithm checksum. All non-digits are ignored.
+ *
+ * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples.
+ */
+public final class LuhnChecksumValidator extends InternalValidator implements Validator,
+ Parcelable {
+ private static final String TAG = "LuhnChecksumValidator";
+
+ private final AutofillId[] mIds;
+
+ /**
+ * Default constructor.
+ *
+ * @param ids id of fields that comprises the number to be checked.
+ */
+ public LuhnChecksumValidator(@NonNull AutofillId... ids) {
+ mIds = Preconditions.checkArrayElementsNotNull(ids, "ids");
+ }
+
+ /**
+ * Checks if the Luhn checksum is valid.
+ *
+ * @param number The number including the checksum
+ */
+ private static boolean isLuhnChecksumValid(@NonNull String number) {
+ int sum = 0;
+ boolean isDoubled = false;
+
+ for (int i = number.length() - 1; i >= 0; i--) {
+ final int digit = number.charAt(i) - '0';
+ if (digit < 0 || digit > 9) {
+ // Ignore non-digits
+ continue;
+ }
+
+ int addend;
+ if (isDoubled) {
+ addend = digit * 2;
+ if (addend > 9) {
+ addend -= 9;
+ }
+ } else {
+ addend = digit;
+ }
+ sum += addend;
+ isDoubled = !isDoubled;
+ }
+
+ return sum % 10 == 0;
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public boolean isValid(@NonNull ValueFinder finder) {
+ if (mIds == null || mIds.length == 0) return false;
+
+ final StringBuilder builder = new StringBuilder();
+ for (AutofillId id : mIds) {
+ final String partialNumber = finder.findByAutofillId(id);
+ if (partialNumber == null) {
+ if (sDebug) Log.d(TAG, "No partial number for id " + id);
+ return false;
+ }
+ builder.append(partialNumber);
+ }
+
+ final String number = builder.toString();
+ boolean valid = isLuhnChecksumValid(number);
+ if (sDebug) Log.d(TAG, "isValid(" + number.length() + " chars): " + valid);
+ return valid;
+ }
+
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "LuhnChecksumValidator: [ids=" + Arrays.toString(mIds) + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelableArray(mIds, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<LuhnChecksumValidator> CREATOR =
+ new Parcelable.Creator<LuhnChecksumValidator>() {
+ @Override
+ public LuhnChecksumValidator createFromParcel(Parcel parcel) {
+ return new LuhnChecksumValidator(parcel.readParcelableArray(null, AutofillId.class));
+ }
+
+ @Override
+ public LuhnChecksumValidator[] newArray(int size) {
+ return new LuhnChecksumValidator[size];
+ }
+ };
+}
diff --git a/android/service/autofill/NegationValidator.java b/android/service/autofill/NegationValidator.java
new file mode 100644
index 0000000..2f098e2
--- /dev/null
+++ b/android/service/autofill/NegationValidator.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Validator used to implement a {@code NOT} logical operation.
+ *
+ * @hide
+ */
+final class NegationValidator extends InternalValidator {
+ @NonNull private final InternalValidator mValidator;
+
+ NegationValidator(@NonNull InternalValidator validator) {
+ mValidator = Preconditions.checkNotNull(validator);
+ }
+
+ @Override
+ public boolean isValid(@NonNull ValueFinder finder) {
+ return !mValidator.isValid(finder);
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "NegationValidator: [validator=" + mValidator + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mValidator, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<NegationValidator> CREATOR =
+ new Parcelable.Creator<NegationValidator>() {
+ @Override
+ public NegationValidator createFromParcel(Parcel parcel) {
+ return new NegationValidator(parcel.readParcelable(null));
+ }
+
+ @Override
+ public NegationValidator[] newArray(int size) {
+ return new NegationValidator[size];
+ }
+ };
+}
diff --git a/android/service/autofill/OnClickAction.java b/android/service/autofill/OnClickAction.java
new file mode 100644
index 0000000..8597a88
--- /dev/null
+++ b/android/service/autofill/OnClickAction.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 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.autofill;
+
+/**
+ * Class used to define an action to be performed when a child view in a
+ * {@link android.widget.RemoteViews presentation} is clicked.
+ *
+ * <p>Typically used to switch the visibility of other views in a
+ * {@link CustomDescription custom save UI}.
+ *
+ * <p><b>Note:</b> This interface is not meant to be implemented by app developers; only
+ * implementations provided by the Android System can be used in other Autofill APIs.
+ */
+public interface OnClickAction {
+}
diff --git a/android/service/autofill/OptionalValidators.java b/android/service/autofill/OptionalValidators.java
new file mode 100644
index 0000000..7189c88
--- /dev/null
+++ b/android/service/autofill/OptionalValidators.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Compound validator that returns {@code true} on {@link #isValid(ValueFinder)} if any
+ * of its subvalidators returns {@code true} as well.
+ *
+ * <p>Used to implement an {@code OR} logical operation.
+ *
+ * @hide
+ */
+final class OptionalValidators extends InternalValidator {
+
+ private static final String TAG = "OptionalValidators";
+
+ @NonNull private final InternalValidator[] mValidators;
+
+ OptionalValidators(@NonNull InternalValidator[] validators) {
+ mValidators = Preconditions.checkArrayElementsNotNull(validators, "validators");
+ }
+
+ @Override
+ public boolean isValid(@NonNull ValueFinder finder) {
+ for (InternalValidator validator : mValidators) {
+ final boolean valid = validator.isValid(finder);
+ if (sDebug) Log.d(TAG, "isValid(" + validator + "): " + valid);
+ if (valid) return true;
+ }
+
+ return false;
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return new StringBuilder("OptionalValidators: [validators=").append(mValidators)
+ .append("]")
+ .toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelableArray(mValidators, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<OptionalValidators> CREATOR =
+ new Parcelable.Creator<OptionalValidators>() {
+ @Override
+ public OptionalValidators createFromParcel(Parcel parcel) {
+ return new OptionalValidators(parcel
+ .readParcelableArray(null, InternalValidator.class));
+ }
+
+ @Override
+ public OptionalValidators[] newArray(int size) {
+ return new OptionalValidators[size];
+ }
+ };
+}
diff --git a/android/service/autofill/RegexValidator.java b/android/service/autofill/RegexValidator.java
new file mode 100644
index 0000000..8cb67d0
--- /dev/null
+++ b/android/service/autofill/RegexValidator.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.autofill.AutofillId;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.regex.Pattern;
+
+/**
+ * Defines if a field is valid based on a regular expression (regex).
+ *
+ * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples.
+ */
+public final class RegexValidator extends InternalValidator implements Validator, Parcelable {
+
+ private static final String TAG = "RegexValidator";
+
+ private final AutofillId mId;
+ private final Pattern mRegex;
+
+ /**
+ * Default constructor.
+ *
+ * @param id id of the field whose regex is applied to.
+ * @param regex regular expression that defines the result of the validator: if the regex
+ * matches the contents of the field identified by {@code id}, it returns {@code true};
+ * otherwise, it returns {@code false}.
+ */
+ public RegexValidator(@NonNull AutofillId id, @NonNull Pattern regex) {
+ mId = Preconditions.checkNotNull(id);
+ mRegex = Preconditions.checkNotNull(regex);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ public boolean isValid(@NonNull ValueFinder finder) {
+ final String value = finder.findByAutofillId(mId);
+ if (value == null) {
+ Log.w(TAG, "No view for id " + mId);
+ return false;
+ }
+
+ final boolean valid = mRegex.matcher(value).matches();
+ if (sDebug) Log.d(TAG, "isValid(): " + valid);
+ return valid;
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "RegexValidator: [id=" + mId + ", regex=" + mRegex + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeParcelable(mId, flags);
+ parcel.writeSerializable(mRegex);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<RegexValidator> CREATOR =
+ new Parcelable.Creator<RegexValidator>() {
+ @Override
+ public RegexValidator createFromParcel(Parcel parcel) {
+ return new RegexValidator(parcel.readParcelable(null),
+ (Pattern) parcel.readSerializable());
+ }
+
+ @Override
+ public RegexValidator[] newArray(int size) {
+ return new RegexValidator[size];
+ }
+ };
+}
diff --git a/android/service/autofill/RequiredValidators.java b/android/service/autofill/RequiredValidators.java
new file mode 100644
index 0000000..619eba0
--- /dev/null
+++ b/android/service/autofill/RequiredValidators.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Compound validator that only returns {@code true} on {@link #isValid(ValueFinder)} if all
+ * of its subvalidators return {@code true} as well.
+ *
+ * <p>Used to implement an {@code AND} logical operation.
+ *
+ * @hide
+ */
+final class RequiredValidators extends InternalValidator {
+
+ private static final String TAG = "RequiredValidators";
+
+ @NonNull private final InternalValidator[] mValidators;
+
+ RequiredValidators(@NonNull InternalValidator[] validators) {
+ mValidators = Preconditions.checkArrayElementsNotNull(validators, "validators");
+ }
+
+ @Override
+ public boolean isValid(@NonNull ValueFinder finder) {
+ for (InternalValidator validator : mValidators) {
+ final boolean valid = validator.isValid(finder);
+ if (sDebug) Log.d(TAG, "isValid(" + validator + "): " + valid);
+ if (!valid) return false;
+ }
+ return true;
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return new StringBuilder("RequiredValidators: [validators=").append(mValidators)
+ .append("]")
+ .toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelableArray(mValidators, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<RequiredValidators> CREATOR =
+ new Parcelable.Creator<RequiredValidators>() {
+ @Override
+ public RequiredValidators createFromParcel(Parcel parcel) {
+ return new RequiredValidators(parcel
+ .readParcelableArray(null, InternalValidator.class));
+ }
+
+ @Override
+ public RequiredValidators[] newArray(int size) {
+ return new RequiredValidators[size];
+ }
+ };
+}
diff --git a/android/service/autofill/Sanitizer.java b/android/service/autofill/Sanitizer.java
new file mode 100644
index 0000000..8a1310d
--- /dev/null
+++ b/android/service/autofill/Sanitizer.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+/**
+ * Helper class used to sanitize user input before using it in a save request.
+ *
+ * <p>Typically used to avoid displaying the save UI for values that are autofilled but reformatted
+ * by the app—for example, if the autofill service sends a credit card number
+ * value as "004815162342108" and the app automatically changes it to "0048 1516 2342 108".
+ *
+ * <p><b>Note:</b> This interface is not meant to be implemented by app developers; only
+ * implementations provided by the Android System can be used in other Autofill APIs.
+ */
+public interface Sanitizer {
+}
diff --git a/android/service/autofill/SaveCallback.java b/android/service/autofill/SaveCallback.java
new file mode 100644
index 0000000..1753ecf
--- /dev/null
+++ b/android/service/autofill/SaveCallback.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 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.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.IntentSender;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Handles save requests from the {@link AutofillService} into the {@link Activity} being
+ * autofilled.
+ */
+public final class SaveCallback {
+
+ private static final String TAG = "SaveCallback";
+
+ private final ISaveCallback mCallback;
+ private boolean mCalled;
+
+ /** @hide */
+ SaveCallback(ISaveCallback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Notifies the Android System that an
+ * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} was successfully handled
+ * by the service.
+ *
+ * @throws IllegalStateException if this method, {@link #onSuccess(IntentSender)}, or
+ * {@link #onFailure(CharSequence)} was already called.
+ */
+ public void onSuccess() {
+ onSuccessInternal(null);
+ }
+
+ /**
+ * Notifies the Android System that an
+ * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} was successfully handled
+ * by the service.
+ *
+ * <p>This method is useful when the service requires extra work—for example, launching an
+ * activity asking the user to authenticate first —before it can process the request,
+ * as the intent will be launched from the context of the activity being autofilled and hence
+ * will be part of that activity's stack.
+ *
+ * @param intentSender intent that will be launched from the context of activity being
+ * autofilled.
+ *
+ * @throws IllegalStateException if this method, {@link #onSuccess()},
+ * or {@link #onFailure(CharSequence)} was already called.
+ */
+ public void onSuccess(@NonNull IntentSender intentSender) {
+ onSuccessInternal(Preconditions.checkNotNull(intentSender));
+ }
+
+ private void onSuccessInternal(@Nullable IntentSender intentSender) {
+ assertNotCalled();
+ mCalled = true;
+ try {
+ mCallback.onSuccess(intentSender);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+
+
+
+ /**
+ * Notifies the Android System that an
+ * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} could not be handled
+ * by the service.
+ *
+ * <p>This method is just used for logging purposes, the Android System won't call the service
+ * again in case of failures—if you need to recover from the failure, just save the
+ * {@link SaveRequest} and try again later.
+ *
+ * <p><b>Note: </b>for apps targeting {@link android.os.Build.VERSION_CODES#Q} or higher, this
+ * method just logs the message on {@code logcat}; for apps targetting older SDKs, it also
+ * displays the message to user using a {@link android.widget.Toast}.
+ *
+ * @param message error message. <b>Note: </b> this message should <b>not</b> contain PII
+ * (Personally Identifiable Information, such as username or email address).
+ *
+ * @throws IllegalStateException if this method, {@link #onSuccess()},
+ * or {@link #onSuccess(IntentSender)} was already called.
+ */
+ public void onFailure(CharSequence message) {
+ Log.w(TAG, "onFailure(): " + message);
+ assertNotCalled();
+ mCalled = true;
+ try {
+ mCallback.onFailure(message);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ private void assertNotCalled() {
+ if (mCalled) {
+ throw new IllegalStateException("Already called");
+ }
+ }
+}
diff --git a/android/service/autofill/SaveInfo.java b/android/service/autofill/SaveInfo.java
new file mode 100644
index 0000000..e640eec
--- /dev/null
+++ b/android/service/autofill/SaveInfo.java
@@ -0,0 +1,924 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.service.autofill.AutofillServiceHelper.assertValid;
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.IntentSender;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.DebugUtils;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * Information used to indicate that an {@link AutofillService} is interested on saving the
+ * user-inputed data for future use, through a
+ * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}
+ * call.
+ *
+ * <p>A {@link SaveInfo} is always associated with a {@link FillResponse}, and it contains at least
+ * two pieces of information:
+ *
+ * <ol>
+ * <li>The type(s) of user data (like password or credit card info) that would be saved.
+ * <li>The minimum set of views (represented by their {@link AutofillId}) that need to be changed
+ * to trigger a save request.
+ * </ol>
+ *
+ * <p>Typically, the {@link SaveInfo} contains the same {@code id}s as the {@link Dataset}:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder()
+ * .setValue(id1, AutofillValue.forText("homer"), createPresentation("homer")) // username
+ * .setValue(id2, AutofillValue.forText("D'OH!"), createPresentation("password for homer")) // password
+ * .build())
+ * .setSaveInfo(new SaveInfo.Builder(
+ * SaveInfo.SAVE_DATA_TYPE_USERNAME | SaveInfo.SAVE_DATA_TYPE_PASSWORD,
+ * new AutofillId[] { id1, id2 }).build())
+ * .build();
+ * </pre>
+ *
+ * <p>The save type flags are used to display the appropriate strings in the autofill save UI.
+ * You can pass multiple values, but try to keep it short if possible. In the above example, just
+ * {@code SaveInfo.SAVE_DATA_TYPE_PASSWORD} would be enough.
+ *
+ * <p>There might be cases where the {@link AutofillService} knows how to fill the screen,
+ * but the user has no data for it. In that case, the {@link FillResponse} should contain just the
+ * {@link SaveInfo}, but no {@link Dataset Datasets}:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_PASSWORD,
+ * new AutofillId[] { id1, id2 }).build())
+ * .build();
+ * </pre>
+ *
+ * <p>There might be cases where the user data in the {@link AutofillService} is enough
+ * to populate some fields but not all, and the service would still be interested on saving the
+ * other fields. In that case, the service could set the
+ * {@link SaveInfo.Builder#setOptionalIds(AutofillId[])} as well:
+ *
+ * <pre class="prettyprint">
+ * new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder()
+ * .setValue(id1, AutofillValue.forText("742 Evergreen Terrace"),
+ * createPresentation("742 Evergreen Terrace")) // street
+ * .setValue(id2, AutofillValue.forText("Springfield"),
+ * createPresentation("Springfield")) // city
+ * .build())
+ * .setSaveInfo(new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_ADDRESS,
+ * new AutofillId[] { id1, id2 }) // street and city
+ * .setOptionalIds(new AutofillId[] { id3, id4 }) // state and zipcode
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <a name="TriggeringSaveRequest"></a>
+ * <h3>Triggering a save request</h3>
+ *
+ * <p>The {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} can be triggered after
+ * any of the following events:
+ * <ul>
+ * <li>The {@link Activity} finishes.
+ * <li>The app explicitly calls {@link AutofillManager#commit()}.
+ * <li>All required views become invisible (if the {@link SaveInfo} was created with the
+ * {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} flag).
+ * <li>The user clicks a specific view (defined by {@link Builder#setTriggerId(AutofillId)}.
+ * </ul>
+ *
+ * <p>But it is only triggered when all conditions below are met:
+ * <ul>
+ * <li>The {@link SaveInfo} associated with the {@link FillResponse} is not {@code null} neither
+ * has the {@link #FLAG_DELAY_SAVE} flag.
+ * <li>The {@link AutofillValue}s of all required views (as set by the {@code requiredIds} passed
+ * to the {@link SaveInfo.Builder} constructor are not empty.
+ * <li>The {@link AutofillValue} of at least one view (be it required or optional) has changed
+ * (i.e., it's neither the same value passed in a {@link Dataset}, nor the initial value
+ * presented in the view).
+ * <li>There is no {@link Dataset} in the last {@link FillResponse} that completely matches the
+ * screen state (i.e., all required and optional fields in the dataset have the same value as
+ * the fields in the screen).
+ * <li>The user explicitly tapped the autofill save UI asking to save data for autofill.
+ * </ul>
+ *
+ * <a name="CustomizingSaveUI"></a>
+ * <h3>Customizing the autofill save UI</h3>
+ *
+ * <p>The service can also customize some aspects of the autofill save UI:
+ * <ul>
+ * <li>Add a simple subtitle by calling {@link Builder#setDescription(CharSequence)}.
+ * <li>Add a customized subtitle by calling
+ * {@link Builder#setCustomDescription(CustomDescription)}.
+ * <li>Customize the button used to reject the save request by calling
+ * {@link Builder#setNegativeAction(int, IntentSender)}.
+ * <li>Decide whether the UI should be shown based on the user input validation by calling
+ * {@link Builder#setValidator(Validator)}.
+ * </ul>
+ */
+public final class SaveInfo implements Parcelable {
+
+ /**
+ * Type used when the service can save the contents of a screen, but cannot describe what
+ * the content is for.
+ */
+ public static final int SAVE_DATA_TYPE_GENERIC = 0x0;
+
+ /**
+ * Type used when the {@link FillResponse} represents user credentials that have a password.
+ */
+ public static final int SAVE_DATA_TYPE_PASSWORD = 0x01;
+
+ /**
+ * Type used on when the {@link FillResponse} represents a physical address (such as street,
+ * city, state, etc).
+ */
+ public static final int SAVE_DATA_TYPE_ADDRESS = 0x02;
+
+ /**
+ * Type used when the {@link FillResponse} represents a credit card.
+ */
+ public static final int SAVE_DATA_TYPE_CREDIT_CARD = 0x04;
+
+ /**
+ * Type used when the {@link FillResponse} represents just an username, without a password.
+ */
+ public static final int SAVE_DATA_TYPE_USERNAME = 0x08;
+
+ /**
+ * Type used when the {@link FillResponse} represents just an email address, without a password.
+ */
+ public static final int SAVE_DATA_TYPE_EMAIL_ADDRESS = 0x10;
+
+ /**
+ * Type used when the {@link FillResponse} represents a debit card.
+ */
+ public static final int SAVE_DATA_TYPE_DEBIT_CARD = 0x20;
+
+ /**
+ * Type used when the {@link FillResponse} represents a payment card except for credit and
+ * debit cards.
+ */
+ public static final int SAVE_DATA_TYPE_PAYMENT_CARD = 0x40;
+
+ /**
+ * Type used when the {@link FillResponse} represents a card that does not a specified card or
+ * cannot identify what the card is for.
+ */
+ public static final int SAVE_DATA_TYPE_GENERIC_CARD = 0x80;
+
+ /**
+ * Style for the negative button of the save UI to cancel the
+ * save operation. In this case, the user tapping the negative
+ * button signals that they would prefer to not save the filled
+ * content.
+ */
+ public static final int NEGATIVE_BUTTON_STYLE_CANCEL = 0;
+
+ /**
+ * Style for the negative button of the save UI to reject the
+ * save operation. This could be useful if the user needs to
+ * opt-in your service and the save prompt is an advertisement
+ * of the potential value you can add to the user. In this
+ * case, the user tapping the negative button sends a strong
+ * signal that the feature may not be useful and you may
+ * consider some backoff strategy.
+ */
+ public static final int NEGATIVE_BUTTON_STYLE_REJECT = 1;
+
+ /**
+ * Style for the negative button of the save UI to never do the
+ * save operation. This means that the user does not need to save
+ * any data on this activity or application. Once the user tapping
+ * the negative button, the service should never trigger the save
+ * UI again. In addition to this, must consider providing restore
+ * options for the user.
+ */
+ public static final int NEGATIVE_BUTTON_STYLE_NEVER = 2;
+
+ /** @hide */
+ @IntDef(prefix = { "NEGATIVE_BUTTON_STYLE_" }, value = {
+ NEGATIVE_BUTTON_STYLE_CANCEL,
+ NEGATIVE_BUTTON_STYLE_REJECT,
+ NEGATIVE_BUTTON_STYLE_NEVER
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface NegativeButtonStyle{}
+
+ /**
+ * Style for the positive button of save UI to request the save operation.
+ * In this case, the user tapping the positive button signals that they
+ * agrees to save the filled content.
+ */
+ public static final int POSITIVE_BUTTON_STYLE_SAVE = 0;
+
+ /**
+ * Style for the positive button of save UI to have next action before the save operation.
+ * This could be useful if the filled content contains sensitive personally identifiable
+ * information and then requires user confirmation or verification. In this case, the user
+ * tapping the positive button signals that they would complete the next required action
+ * to save the filled content.
+ */
+ public static final int POSITIVE_BUTTON_STYLE_CONTINUE = 1;
+
+ /** @hide */
+ @IntDef(prefix = { "POSITIVE_BUTTON_STYLE_" }, value = {
+ POSITIVE_BUTTON_STYLE_SAVE,
+ POSITIVE_BUTTON_STYLE_CONTINUE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface PositiveButtonStyle{}
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "SAVE_DATA_TYPE_" }, value = {
+ SAVE_DATA_TYPE_GENERIC,
+ SAVE_DATA_TYPE_PASSWORD,
+ SAVE_DATA_TYPE_ADDRESS,
+ SAVE_DATA_TYPE_CREDIT_CARD,
+ SAVE_DATA_TYPE_USERNAME,
+ SAVE_DATA_TYPE_EMAIL_ADDRESS,
+ SAVE_DATA_TYPE_DEBIT_CARD,
+ SAVE_DATA_TYPE_PAYMENT_CARD,
+ SAVE_DATA_TYPE_GENERIC_CARD
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface SaveDataType{}
+
+ /**
+ * Usually, a save request is only automatically <a href="#TriggeringSaveRequest">triggered</a>
+ * once the {@link Activity} finishes. If this flag is set, it is triggered once all saved views
+ * become invisible.
+ */
+ public static final int FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 0x1;
+
+ /**
+ * By default, a save request is automatically <a href="#TriggeringSaveRequest">triggered</a>
+ * once the {@link Activity} finishes. If this flag is set, finishing the activity doesn't
+ * trigger a save request.
+ *
+ * <p>This flag is typically used in conjunction with {@link Builder#setTriggerId(AutofillId)}.
+ */
+ public static final int FLAG_DONT_SAVE_ON_FINISH = 0x2;
+
+
+ /**
+ * Postpone the autofill save UI.
+ *
+ * <p>If flag is set, the autofill save UI is not triggered when the
+ * autofill context associated with the response associated with this {@link SaveInfo} is
+ * committed (with {@link AutofillManager#commit()}). Instead, the {@link FillContext}
+ * is delivered in future fill requests (with {@link
+ * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)})
+ * and save request (with {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)})
+ * of an activity belonging to the same task.
+ *
+ * <p>This flag should be used when the service detects that the application uses
+ * multiple screens to implement an autofillable workflow (for example, one screen for the
+ * username field, another for password).
+ */
+ // TODO(b/113281366): improve documentation: add example, document relationship with other
+ // flagss, etc...
+ public static final int FLAG_DELAY_SAVE = 0x4;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE,
+ FLAG_DONT_SAVE_ON_FINISH,
+ FLAG_DELAY_SAVE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface SaveInfoFlags{}
+
+ private final @SaveDataType int mType;
+ private final @NegativeButtonStyle int mNegativeButtonStyle;
+ private final @PositiveButtonStyle int mPositiveButtonStyle;
+ private final IntentSender mNegativeActionListener;
+ private final AutofillId[] mRequiredIds;
+ private final AutofillId[] mOptionalIds;
+ private final CharSequence mDescription;
+ private final int mFlags;
+ private final CustomDescription mCustomDescription;
+ private final InternalValidator mValidator;
+ private final InternalSanitizer[] mSanitizerKeys;
+ private final AutofillId[][] mSanitizerValues;
+ private final AutofillId mTriggerId;
+
+ private SaveInfo(Builder builder) {
+ mType = builder.mType;
+ mNegativeButtonStyle = builder.mNegativeButtonStyle;
+ mNegativeActionListener = builder.mNegativeActionListener;
+ mPositiveButtonStyle = builder.mPositiveButtonStyle;
+ mRequiredIds = builder.mRequiredIds;
+ mOptionalIds = builder.mOptionalIds;
+ mDescription = builder.mDescription;
+ mFlags = builder.mFlags;
+ mCustomDescription = builder.mCustomDescription;
+ mValidator = builder.mValidator;
+ if (builder.mSanitizers == null) {
+ mSanitizerKeys = null;
+ mSanitizerValues = null;
+ } else {
+ final int size = builder.mSanitizers.size();
+ mSanitizerKeys = new InternalSanitizer[size];
+ mSanitizerValues = new AutofillId[size][];
+ for (int i = 0; i < size; i++) {
+ mSanitizerKeys[i] = builder.mSanitizers.keyAt(i);
+ mSanitizerValues[i] = builder.mSanitizers.valueAt(i);
+ }
+ }
+ mTriggerId = builder.mTriggerId;
+ }
+
+ /** @hide */
+ public @NegativeButtonStyle int getNegativeActionStyle() {
+ return mNegativeButtonStyle;
+ }
+
+ /** @hide */
+ public @Nullable IntentSender getNegativeActionListener() {
+ return mNegativeActionListener;
+ }
+
+ /** @hide */
+ public @PositiveButtonStyle int getPositiveActionStyle() {
+ return mPositiveButtonStyle;
+ }
+
+ /** @hide */
+ public @Nullable AutofillId[] getRequiredIds() {
+ return mRequiredIds;
+ }
+
+ /** @hide */
+ public @Nullable AutofillId[] getOptionalIds() {
+ return mOptionalIds;
+ }
+
+ /** @hide */
+ public @SaveDataType int getType() {
+ return mType;
+ }
+
+ /** @hide */
+ public @SaveInfoFlags int getFlags() {
+ return mFlags;
+ }
+
+ /** @hide */
+ public CharSequence getDescription() {
+ return mDescription;
+ }
+
+ /** @hide */
+ @Nullable
+ public CustomDescription getCustomDescription() {
+ return mCustomDescription;
+ }
+
+ /** @hide */
+ @Nullable
+ public InternalValidator getValidator() {
+ return mValidator;
+ }
+
+ /** @hide */
+ @Nullable
+ public InternalSanitizer[] getSanitizerKeys() {
+ return mSanitizerKeys;
+ }
+
+ /** @hide */
+ @Nullable
+ public AutofillId[][] getSanitizerValues() {
+ return mSanitizerValues;
+ }
+
+ /** @hide */
+ @Nullable
+ public AutofillId getTriggerId() {
+ return mTriggerId;
+ }
+
+ /**
+ * A builder for {@link SaveInfo} objects.
+ */
+ public static final class Builder {
+
+ private final @SaveDataType int mType;
+ private @NegativeButtonStyle int mNegativeButtonStyle = NEGATIVE_BUTTON_STYLE_CANCEL;
+ private @PositiveButtonStyle int mPositiveButtonStyle = POSITIVE_BUTTON_STYLE_SAVE;
+ private IntentSender mNegativeActionListener;
+ private final AutofillId[] mRequiredIds;
+ private AutofillId[] mOptionalIds;
+ private CharSequence mDescription;
+ private boolean mDestroyed;
+ private int mFlags;
+ private CustomDescription mCustomDescription;
+ private InternalValidator mValidator;
+ private ArrayMap<InternalSanitizer, AutofillId[]> mSanitizers;
+ // Set used to validate against duplicate ids.
+ private ArraySet<AutofillId> mSanitizerIds;
+ private AutofillId mTriggerId;
+
+ /**
+ * Creates a new builder.
+ *
+ * @param type the type of information the associated {@link FillResponse} represents. It
+ * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC},
+ * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_DEBIT_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_PAYMENT_CARD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_GENERIC_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_USERNAME},
+ * or {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}.
+ * @param requiredIds ids of all required views that will trigger a save request.
+ *
+ * <p>See {@link SaveInfo} for more info.
+ *
+ * @throws IllegalArgumentException if {@code requiredIds} is {@code null} or empty, or if
+ * it contains any {@code null} entry.
+ */
+ public Builder(@SaveDataType int type, @NonNull AutofillId[] requiredIds) {
+ mType = type;
+ mRequiredIds = assertValid(requiredIds);
+ }
+
+ /**
+ * Creates a new builder when no id is required.
+ *
+ * <p>When using this builder, caller must call {@link #setOptionalIds(AutofillId[])} before
+ * calling {@link #build()}.
+ *
+ * @param type the type of information the associated {@link FillResponse} represents. It
+ * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC},
+ * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_DEBIT_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_PAYMENT_CARD},
+ * {@link SaveInfo#SAVE_DATA_TYPE_GENERIC_CARD}, {@link SaveInfo#SAVE_DATA_TYPE_USERNAME},
+ * or {@link SaveInfo#SAVE_DATA_TYPE_EMAIL_ADDRESS}.
+ *
+ * <p>See {@link SaveInfo} for more info.
+ */
+ public Builder(@SaveDataType int type) {
+ mType = type;
+ mRequiredIds = null;
+ }
+
+ /**
+ * Sets flags changing the save behavior.
+ *
+ * @param flags {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE},
+ * {@link #FLAG_DONT_SAVE_ON_FINISH}, {@link #FLAG_DELAY_SAVE}, or {@code 0}.
+ * @return This builder.
+ */
+ public @NonNull Builder setFlags(@SaveInfoFlags int flags) {
+ throwIfDestroyed();
+
+ mFlags = Preconditions.checkFlagsArgument(flags,
+ FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE | FLAG_DONT_SAVE_ON_FINISH
+ | FLAG_DELAY_SAVE);
+ return this;
+ }
+
+ /**
+ * Sets the ids of additional, optional views the service would be interested to save.
+ *
+ * <p>See {@link SaveInfo} for more info.
+ *
+ * @param ids The ids of the optional views.
+ * @return This builder.
+ *
+ * @throws IllegalArgumentException if {@code ids} is {@code null} or empty, or if
+ * it contains any {@code null} entry.
+ */
+ public @NonNull Builder setOptionalIds(@NonNull AutofillId[] ids) {
+ throwIfDestroyed();
+ mOptionalIds = assertValid(ids);
+ return this;
+ }
+
+ /**
+ * Sets an optional description to be shown in the UI when the user is asked to save.
+ *
+ * <p>Typically, it describes how the data will be stored by the service, so it can help
+ * users to decide whether they can trust the service to save their data.
+ *
+ * @param description a succint description.
+ * @return This Builder.
+ *
+ * @throws IllegalStateException if this call was made after calling
+ * {@link #setCustomDescription(CustomDescription)}.
+ */
+ public @NonNull Builder setDescription(@Nullable CharSequence description) {
+ throwIfDestroyed();
+ Preconditions.checkState(mCustomDescription == null,
+ "Can call setDescription() or setCustomDescription(), but not both");
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Sets a custom description to be shown in the UI when the user is asked to save.
+ *
+ * <p>Typically used when the service must show more info about the object being saved,
+ * like a credit card logo, masked number, and expiration date.
+ *
+ * @param customDescription the custom description.
+ * @return This Builder.
+ *
+ * @throws IllegalStateException if this call was made after calling
+ * {@link #setDescription(CharSequence)}.
+ */
+ public @NonNull Builder setCustomDescription(@NonNull CustomDescription customDescription) {
+ throwIfDestroyed();
+ Preconditions.checkState(mDescription == null,
+ "Can call setDescription() or setCustomDescription(), but not both");
+ mCustomDescription = customDescription;
+ return this;
+ }
+
+ /**
+ * Sets the style and listener for the negative save action.
+ *
+ * <p>This allows an autofill service to customize the style and be
+ * notified when the user selects the negative action in the save
+ * UI. Note that selecting the negative action regardless of its style
+ * and listener being customized would dismiss the save UI and if a
+ * custom listener intent is provided then this intent is
+ * started. The default style is {@link #NEGATIVE_BUTTON_STYLE_CANCEL}</p>
+ *
+ * @param style The action style.
+ * @param listener The action listener.
+ * @return This builder.
+ *
+ * @see #NEGATIVE_BUTTON_STYLE_CANCEL
+ * @see #NEGATIVE_BUTTON_STYLE_REJECT
+ * @see #NEGATIVE_BUTTON_STYLE_NEVER
+ *
+ * @throws IllegalArgumentException If the style is invalid
+ */
+ public @NonNull Builder setNegativeAction(@NegativeButtonStyle int style,
+ @Nullable IntentSender listener) {
+ throwIfDestroyed();
+ Preconditions.checkArgumentInRange(style, NEGATIVE_BUTTON_STYLE_CANCEL,
+ NEGATIVE_BUTTON_STYLE_NEVER, "style");
+ mNegativeButtonStyle = style;
+ mNegativeActionListener = listener;
+ return this;
+ }
+
+ /**
+ * Sets the style for the positive save action.
+ *
+ * <p>This allows an autofill service to customize the style of the
+ * positive action in the save UI. Note that selecting the positive
+ * action regardless of its style would dismiss the save UI and calling
+ * into the {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback) save request}.
+ * The service should take the next action if selecting style
+ * {@link #POSITIVE_BUTTON_STYLE_CONTINUE}. The default style is
+ * {@link #POSITIVE_BUTTON_STYLE_SAVE}
+ *
+ * @param style The action style.
+ * @return This builder.
+ *
+ * @see #POSITIVE_BUTTON_STYLE_SAVE
+ * @see #POSITIVE_BUTTON_STYLE_CONTINUE
+ *
+ * @throws IllegalArgumentException If the style is invalid
+ */
+ public @NonNull Builder setPositiveAction(@PositiveButtonStyle int style) {
+ throwIfDestroyed();
+ Preconditions.checkArgumentInRange(style, POSITIVE_BUTTON_STYLE_SAVE,
+ POSITIVE_BUTTON_STYLE_CONTINUE, "style");
+ mPositiveButtonStyle = style;
+ return this;
+ }
+
+ /**
+ * Sets an object used to validate the user input - if the input is not valid, the
+ * autofill save UI is not shown.
+ *
+ * <p>Typically used to validate credit card numbers. Examples:
+ *
+ * <p>Validator for a credit number that must have exactly 16 digits:
+ *
+ * <pre class="prettyprint">
+ * Validator validator = new RegexValidator(ccNumberId, Pattern.compile(""^\\d{16}$"))
+ * </pre>
+ *
+ * <p>Validator for a credit number that must pass a Luhn checksum and either have
+ * 16 digits, or 15 digits starting with 108:
+ *
+ * <pre class="prettyprint">
+ * import static android.service.autofill.Validators.and;
+ * import static android.service.autofill.Validators.or;
+ *
+ * Validator validator =
+ * and(
+ * new LuhnChecksumValidator(ccNumberId),
+ * or(
+ * new RegexValidator(ccNumberId, Pattern.compile("^\\d{16}$")),
+ * new RegexValidator(ccNumberId, Pattern.compile("^108\\d{12}$"))
+ * )
+ * );
+ * </pre>
+ *
+ * <p><b>Note:</b> the example above is just for illustrative purposes; the same validator
+ * could be created using a single regex for the {@code OR} part:
+ *
+ * <pre class="prettyprint">
+ * Validator validator =
+ * and(
+ * new LuhnChecksumValidator(ccNumberId),
+ * new RegexValidator(ccNumberId, Pattern.compile(""^(\\d{16}|108\\d{12})$"))
+ * );
+ * </pre>
+ *
+ * <p>Validator for a credit number contained in just 4 fields and that must have exactly
+ * 4 digits on each field:
+ *
+ * <pre class="prettyprint">
+ * import static android.service.autofill.Validators.and;
+ *
+ * Validator validator =
+ * and(
+ * new RegexValidator(ccNumberId1, Pattern.compile("^\\d{4}$")),
+ * new RegexValidator(ccNumberId2, Pattern.compile("^\\d{4}$")),
+ * new RegexValidator(ccNumberId3, Pattern.compile("^\\d{4}$")),
+ * new RegexValidator(ccNumberId4, Pattern.compile("^\\d{4}$"))
+ * );
+ * </pre>
+ *
+ * @param validator an implementation provided by the Android System.
+ * @return this builder.
+ *
+ * @throws IllegalArgumentException if {@code validator} is not a class provided
+ * by the Android System.
+ */
+ public @NonNull Builder setValidator(@NonNull Validator validator) {
+ throwIfDestroyed();
+ Preconditions.checkArgument((validator instanceof InternalValidator),
+ "not provided by Android System: " + validator);
+ mValidator = (InternalValidator) validator;
+ return this;
+ }
+
+ /**
+ * Adds a sanitizer for one or more field.
+ *
+ * <p>When a sanitizer is set for a field, the {@link AutofillValue} is sent to the
+ * sanitizer before a save request is <a href="#TriggeringSaveRequest">triggered</a>.
+ *
+ * <p>Typically used to avoid displaying the save UI for values that are autofilled but
+ * reformattedby the app. For example, to remove spaces between every 4 digits of a
+ * credit card number:
+ *
+ * <pre class="prettyprint">
+ * builder.addSanitizer(new TextValueSanitizer(
+ * Pattern.compile("^(\\d{4})\\s?(\\d{4})\\s?(\\d{4})\\s?(\\d{4})$", "$1$2$3$4")),
+ * ccNumberId);
+ * </pre>
+ *
+ * <p>The same sanitizer can be reused to sanitize multiple fields. For example, to trim
+ * both the username and password fields:
+ *
+ * <pre class="prettyprint">
+ * builder.addSanitizer(
+ * new TextValueSanitizer(Pattern.compile("^\\s*(.*)\\s*$"), "$1"),
+ * usernameId, passwordId);
+ * </pre>
+ *
+ * <p>The sanitizer can also be used as an alternative for a
+ * {@link #setValidator(Validator) validator}. If any of the {@code ids} is a
+ * {@link #Builder(int, AutofillId[]) required id} and the {@code sanitizer} fails
+ * because of it, then the save UI is not shown.
+ *
+ * @param sanitizer an implementation provided by the Android System.
+ * @param ids id of fields whose value will be sanitized.
+ * @return this builder.
+ *
+ * @throws IllegalArgumentException if a sanitizer for any of the {@code ids} has already
+ * been added or if {@code ids} is empty.
+ */
+ public @NonNull Builder addSanitizer(@NonNull Sanitizer sanitizer,
+ @NonNull AutofillId... ids) {
+ throwIfDestroyed();
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(ids), "ids cannot be empty or null");
+ Preconditions.checkArgument((sanitizer instanceof InternalSanitizer),
+ "not provided by Android System: " + sanitizer);
+
+ if (mSanitizers == null) {
+ mSanitizers = new ArrayMap<>();
+ mSanitizerIds = new ArraySet<>(ids.length);
+ }
+
+ // Check for duplicates first.
+ for (AutofillId id : ids) {
+ Preconditions.checkArgument(!mSanitizerIds.contains(id), "already added %s", id);
+ mSanitizerIds.add(id);
+ }
+
+ mSanitizers.put((InternalSanitizer) sanitizer, ids);
+
+ return this;
+ }
+
+ /**
+ * Explicitly defines the view that should commit the autofill context when clicked.
+ *
+ * <p>Usually, the save request is only automatically
+ * <a href="#TriggeringSaveRequest">triggered</a> after the activity is
+ * finished or all relevant views become invisible, but there are scenarios where the
+ * autofill context is automatically commited too late
+ * —for example, when the activity manually clears the autofillable views when a
+ * button is tapped. This method can be used to trigger the autofill save UI earlier in
+ * these scenarios.
+ *
+ * <p><b>Note:</b> This method should only be used in scenarios where the automatic workflow
+ * is not enough, otherwise it could trigger the autofill save UI when it should not—
+ * for example, when the user entered invalid credentials for the autofillable views.
+ */
+ public @NonNull Builder setTriggerId(@NonNull AutofillId id) {
+ throwIfDestroyed();
+ mTriggerId = Preconditions.checkNotNull(id);
+ return this;
+ }
+
+ /**
+ * Builds a new {@link SaveInfo} instance.
+ *
+ * @throws IllegalStateException if no
+ * {@link #Builder(int, AutofillId[]) required ids},
+ * or {@link #setOptionalIds(AutofillId[]) optional ids}, or {@link #FLAG_DELAY_SAVE}
+ * were set
+ */
+ public SaveInfo build() {
+ throwIfDestroyed();
+ Preconditions.checkState(
+ !ArrayUtils.isEmpty(mRequiredIds) || !ArrayUtils.isEmpty(mOptionalIds)
+ || (mFlags & FLAG_DELAY_SAVE) != 0,
+ "must have at least one required or optional id or FLAG_DELAYED_SAVE");
+ mDestroyed = true;
+ return new SaveInfo(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ final StringBuilder builder = new StringBuilder("SaveInfo: [type=")
+ .append(DebugUtils.flagsToString(SaveInfo.class, "SAVE_DATA_TYPE_", mType))
+ .append(", requiredIds=").append(Arrays.toString(mRequiredIds))
+ .append(", negative style=").append(DebugUtils.flagsToString(SaveInfo.class,
+ "NEGATIVE_BUTTON_STYLE_", mNegativeButtonStyle))
+ .append(", positive style=").append(DebugUtils.flagsToString(SaveInfo.class,
+ "POSITIVE_BUTTON_STYLE_", mPositiveButtonStyle));
+ if (mOptionalIds != null) {
+ builder.append(", optionalIds=").append(Arrays.toString(mOptionalIds));
+ }
+ if (mDescription != null) {
+ builder.append(", description=").append(mDescription);
+ }
+ if (mFlags != 0) {
+ builder.append(", flags=").append(mFlags);
+ }
+ if (mCustomDescription != null) {
+ builder.append(", customDescription=").append(mCustomDescription);
+ }
+ if (mValidator != null) {
+ builder.append(", validator=").append(mValidator);
+ }
+ if (mSanitizerKeys != null) {
+ builder.append(", sanitizerKeys=").append(mSanitizerKeys.length);
+ }
+ if (mSanitizerValues != null) {
+ builder.append(", sanitizerValues=").append(mSanitizerValues.length);
+ }
+ if (mTriggerId != null) {
+ builder.append(", triggerId=").append(mTriggerId);
+ }
+
+ return builder.append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mType);
+ parcel.writeParcelableArray(mRequiredIds, flags);
+ parcel.writeParcelableArray(mOptionalIds, flags);
+ parcel.writeInt(mNegativeButtonStyle);
+ parcel.writeParcelable(mNegativeActionListener, flags);
+ parcel.writeInt(mPositiveButtonStyle);
+ parcel.writeCharSequence(mDescription);
+ parcel.writeParcelable(mCustomDescription, flags);
+ parcel.writeParcelable(mValidator, flags);
+ parcel.writeParcelableArray(mSanitizerKeys, flags);
+ if (mSanitizerKeys != null) {
+ for (int i = 0; i < mSanitizerValues.length; i++) {
+ parcel.writeParcelableArray(mSanitizerValues[i], flags);
+ }
+ }
+ parcel.writeParcelable(mTriggerId, flags);
+ parcel.writeInt(mFlags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<SaveInfo> CREATOR = new Parcelable.Creator<SaveInfo>() {
+ @Override
+ public SaveInfo createFromParcel(Parcel parcel) {
+
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final int type = parcel.readInt();
+ final AutofillId[] requiredIds = parcel.readParcelableArray(null, AutofillId.class);
+ final Builder builder = requiredIds != null
+ ? new Builder(type, requiredIds)
+ : new Builder(type);
+ final AutofillId[] optionalIds = parcel.readParcelableArray(null, AutofillId.class);
+ if (optionalIds != null) {
+ builder.setOptionalIds(optionalIds);
+ }
+
+ builder.setNegativeAction(parcel.readInt(), parcel.readParcelable(null));
+ builder.setPositiveAction(parcel.readInt());
+ builder.setDescription(parcel.readCharSequence());
+ final CustomDescription customDescripton = parcel.readParcelable(null);
+ if (customDescripton != null) {
+ builder.setCustomDescription(customDescripton);
+ }
+ final InternalValidator validator = parcel.readParcelable(null);
+ if (validator != null) {
+ builder.setValidator(validator);
+ }
+ final InternalSanitizer[] sanitizers =
+ parcel.readParcelableArray(null, InternalSanitizer.class);
+ if (sanitizers != null) {
+ final int size = sanitizers.length;
+ for (int i = 0; i < size; i++) {
+ final AutofillId[] autofillIds =
+ parcel.readParcelableArray(null, AutofillId.class);
+ builder.addSanitizer(sanitizers[i], autofillIds);
+ }
+ }
+ final AutofillId triggerId = parcel.readParcelable(null);
+ if (triggerId != null) {
+ builder.setTriggerId(triggerId);
+ }
+ builder.setFlags(parcel.readInt());
+ return builder.build();
+ }
+
+ @Override
+ public SaveInfo[] newArray(int size) {
+ return new SaveInfo[size];
+ }
+ };
+}
diff --git a/android/service/autofill/SaveRequest.java b/android/service/autofill/SaveRequest.java
new file mode 100644
index 0000000..5dd07c4
--- /dev/null
+++ b/android/service/autofill/SaveRequest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class represents a request to an {@link AutofillService
+ * autofill provider} to save applicable data entered by the user.
+ *
+ * @see AutofillService#onSaveRequest(SaveRequest, SaveCallback)
+ */
+public final class SaveRequest implements Parcelable {
+ private final @NonNull ArrayList<FillContext> mFillContexts;
+ private final @Nullable Bundle mClientState;
+ private final @Nullable ArrayList<String> mDatasetIds;
+
+ /** @hide */
+ public SaveRequest(@NonNull ArrayList<FillContext> fillContexts,
+ @Nullable Bundle clientState, @Nullable ArrayList<String> datasetIds) {
+ mFillContexts = Preconditions.checkNotNull(fillContexts, "fillContexts");
+ mClientState = clientState;
+ mDatasetIds = datasetIds;
+ }
+
+ private SaveRequest(@NonNull Parcel parcel) {
+ this(parcel.createTypedArrayList(FillContext.CREATOR),
+ parcel.readBundle(), parcel.createStringArrayList());
+ }
+
+ /**
+ * Gets the contexts associated with each previous fill request.
+ *
+ * <p><b>Note:</b> Starting on Android {@link android.os.Build.VERSION_CODES#Q}, it could also
+ * include contexts from requests whose {@link SaveInfo} had the
+ * {@link SaveInfo#FLAG_DELAY_SAVE} flag.
+ *
+ * @return The contexts associated with each previous fill request.
+ */
+ public @NonNull List<FillContext> getFillContexts() {
+ return mFillContexts;
+ }
+
+ /**
+ * Gets the latest client state bundle set by the service in a
+ * {@link FillResponse.Builder#setClientState(Bundle) fill response}.
+ *
+ * <p><b>Note:</b> Prior to Android {@link android.os.Build.VERSION_CODES#P}, only client state
+ * bundles set by {@link FillResponse.Builder#setClientState(Bundle)} were considered. On
+ * Android {@link android.os.Build.VERSION_CODES#P} and higher, bundles set in the result of
+ * an authenticated request through the
+ * {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE} extra are
+ * also considered (and take precedence when set).
+ *
+ * @return The client state.
+ */
+ public @Nullable Bundle getClientState() {
+ return mClientState;
+ }
+
+ /**
+ * Gets the ids of the datasets selected by the user, in the order in which they were selected.
+ */
+ @Nullable
+ public List<String> getDatasetIds() {
+ return mDatasetIds;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeTypedList(mFillContexts, flags);
+ parcel.writeBundle(mClientState);
+ parcel.writeStringList(mDatasetIds);
+ }
+
+ public static final @android.annotation.NonNull Creator<SaveRequest> CREATOR =
+ new Creator<SaveRequest>() {
+ @Override
+ public SaveRequest createFromParcel(Parcel parcel) {
+ return new SaveRequest(parcel);
+ }
+
+ @Override
+ public SaveRequest[] newArray(int size) {
+ return new SaveRequest[size];
+ }
+ };
+}
diff --git a/android/service/autofill/TextValueSanitizer.java b/android/service/autofill/TextValueSanitizer.java
new file mode 100644
index 0000000..cc48fcb
--- /dev/null
+++ b/android/service/autofill/TextValueSanitizer.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Slog;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Sanitizes a text {@link AutofillValue} using a regular expression (regex) substitution.
+ *
+ * <p>For example, to remove spaces from groups of 4-digits in a credit card:
+ *
+ * <pre class="prettyprint">
+ * new TextValueSanitizer(Pattern.compile("^(\\d{4})\\s?(\\d{4})\\s?(\\d{4})\\s?(\\d{4})$"),
+ * "$1$2$3$4")
+ * </pre>
+ */
+public final class TextValueSanitizer extends InternalSanitizer implements
+ Sanitizer, Parcelable {
+ private static final String TAG = "TextValueSanitizer";
+
+ private final Pattern mRegex;
+ private final String mSubst;
+
+ /**
+ * Default constructor.
+ *
+ * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
+ * are used to substitute parts of the {@link AutofillValue#getTextValue() text value}.
+ * @param subst the string that substitutes the matched regex, using {@code $} for
+ * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
+ */
+ public TextValueSanitizer(@NonNull Pattern regex, @NonNull String subst) {
+ mRegex = Preconditions.checkNotNull(regex);
+ mSubst = Preconditions.checkNotNull(subst);
+ }
+
+ /** @hide */
+ @Override
+ @TestApi
+ @Nullable
+ public AutofillValue sanitize(@NonNull AutofillValue value) {
+ if (value == null) {
+ Slog.w(TAG, "sanitize() called with null value");
+ return null;
+ }
+ if (!value.isText()) {
+ if (sDebug) Slog.d(TAG, "sanitize() called with non-text value: " + value);
+ return null;
+ }
+
+ final CharSequence text = value.getTextValue();
+
+ try {
+ final Matcher matcher = mRegex.matcher(text);
+ if (!matcher.matches()) {
+ if (sDebug) Slog.d(TAG, "sanitize(): " + mRegex + " failed for " + value);
+ return null;
+ }
+
+ final CharSequence sanitized = matcher.replaceAll(mSubst);
+ return AutofillValue.forText(sanitized);
+ } catch (Exception e) {
+ Slog.w(TAG, "Exception evaluating " + mRegex + "/" + mSubst + ": " + e);
+ return null;
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "TextValueSanitizer: [regex=" + mRegex + ", subst=" + mSubst + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeSerializable(mRegex);
+ parcel.writeString(mSubst);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<TextValueSanitizer> CREATOR =
+ new Parcelable.Creator<TextValueSanitizer>() {
+ @Override
+ public TextValueSanitizer createFromParcel(Parcel parcel) {
+ return new TextValueSanitizer((Pattern) parcel.readSerializable(), parcel.readString());
+ }
+
+ @Override
+ public TextValueSanitizer[] newArray(int size) {
+ return new TextValueSanitizer[size];
+ }
+ };
+}
diff --git a/android/service/autofill/Transformation.java b/android/service/autofill/Transformation.java
new file mode 100644
index 0000000..de43955
--- /dev/null
+++ b/android/service/autofill/Transformation.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+/**
+ * Helper class used to change a child view of a {@link android.widget.RemoteViews presentation
+ * template} at runtime, using the values of fields contained in the screen.
+ *
+ * <p>Typically used by {@link CustomDescription} to provide a customized autofill save UI.
+ *
+ * <p><b>Note:</b> This interface is not meant to be implemented by app developers; only
+ * implementations provided by the Android System can be used in other Autofill APIs.
+ */
+public interface Transformation {
+}
diff --git a/android/service/autofill/UserData.java b/android/service/autofill/UserData.java
new file mode 100644
index 0000000..7814f70
--- /dev/null
+++ b/android/service/autofill/UserData.java
@@ -0,0 +1,537 @@
+/*
+ * Copyright 2017 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.autofill;
+
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH;
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.app.ActivityThread;
+import android.content.ContentResolver;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.Settings;
+import android.service.autofill.FieldClassification.Match;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.Helper;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Defines the user data used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ */
+public final class UserData implements FieldClassificationUserData, Parcelable {
+
+ private static final String TAG = "UserData";
+
+ private static final int DEFAULT_MAX_USER_DATA_SIZE = 50;
+ private static final int DEFAULT_MAX_CATEGORY_COUNT = 10;
+ private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10;
+ private static final int DEFAULT_MIN_VALUE_LENGTH = 3;
+ private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
+
+ private final String mId;
+ private final String[] mCategoryIds;
+ private final String[] mValues;
+
+ private final String mDefaultAlgorithm;
+ private final Bundle mDefaultArgs;
+ private final ArrayMap<String, String> mCategoryAlgorithms;
+ private final ArrayMap<String, Bundle> mCategoryArgs;
+
+ private UserData(Builder builder) {
+ mId = builder.mId;
+ mCategoryIds = new String[builder.mCategoryIds.size()];
+ builder.mCategoryIds.toArray(mCategoryIds);
+ mValues = new String[builder.mValues.size()];
+ builder.mValues.toArray(mValues);
+ builder.mValues.toArray(mValues);
+
+ mDefaultAlgorithm = builder.mDefaultAlgorithm;
+ mDefaultArgs = builder.mDefaultArgs;
+ mCategoryAlgorithms = builder.mCategoryAlgorithms;
+ mCategoryArgs = builder.mCategoryArgs;
+ }
+
+ /**
+ * Gets the name of the default algorithm that is used to calculate
+ * {@link Match#getScore()} match scores}.
+ */
+ @Nullable
+ @Override
+ public String getFieldClassificationAlgorithm() {
+ return mDefaultAlgorithm;
+ }
+
+ /** @hide */
+ @Override
+ public Bundle getDefaultFieldClassificationArgs() {
+ return mDefaultArgs;
+ }
+
+ /**
+ * Gets the name of the algorithm corresponding to the specific autofill category
+ * that is used to calculate {@link Match#getScore() match scores}
+ *
+ * @param categoryId autofill field category
+ *
+ * @return String name of algorithm, null if none found.
+ */
+ @Nullable
+ @Override
+ public String getFieldClassificationAlgorithmForCategory(@NonNull String categoryId) {
+ Preconditions.checkNotNull(categoryId);
+ if (mCategoryAlgorithms == null || !mCategoryAlgorithms.containsKey(categoryId)) {
+ return null;
+ }
+ return mCategoryAlgorithms.get(categoryId);
+ }
+
+ /**
+ * Gets the id.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /** @hide */
+ @Override
+ public String[] getCategoryIds() {
+ return mCategoryIds;
+ }
+
+ /** @hide */
+ @Override
+ public String[] getValues() {
+ return mValues;
+ }
+
+ /** @hide */
+ @TestApi
+ @Override
+ public ArrayMap<String, String> getFieldClassificationAlgorithms() {
+ return mCategoryAlgorithms;
+ }
+
+ /** @hide */
+ @Override
+ public ArrayMap<String, Bundle> getFieldClassificationArgs() {
+ return mCategoryArgs;
+ }
+
+ /** @hide */
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("id: "); pw.print(mId);
+ pw.print(prefix); pw.print("Default Algorithm: "); pw.print(mDefaultAlgorithm);
+ pw.print(prefix); pw.print("Default Args"); pw.print(mDefaultArgs);
+ if (mCategoryAlgorithms != null && mCategoryAlgorithms.size() > 0) {
+ pw.print(prefix); pw.print("Algorithms per category: ");
+ for (int i = 0; i < mCategoryAlgorithms.size(); i++) {
+ pw.print(prefix); pw.print(prefix); pw.print(mCategoryAlgorithms.keyAt(i));
+ pw.print(": "); pw.println(Helper.getRedacted(mCategoryAlgorithms.valueAt(i)));
+ pw.print("args="); pw.print(mCategoryArgs.get(mCategoryAlgorithms.keyAt(i)));
+ }
+ }
+ // Cannot disclose field ids or values because they could contain PII
+ pw.print(prefix); pw.print("Field ids size: "); pw.println(mCategoryIds.length);
+ for (int i = 0; i < mCategoryIds.length; i++) {
+ pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
+ pw.println(Helper.getRedacted(mCategoryIds[i]));
+ }
+ pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length);
+ for (int i = 0; i < mValues.length; i++) {
+ pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
+ pw.println(Helper.getRedacted(mValues[i]));
+ }
+ }
+
+ /** @hide */
+ public static void dumpConstraints(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize());
+ pw.print(prefix); pw.print("maxFieldClassificationIdsSize: ");
+ pw.println(getMaxFieldClassificationIdsSize());
+ pw.print(prefix); pw.print("maxCategoryCount: "); pw.println(getMaxCategoryCount());
+ pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength());
+ pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength());
+ }
+
+ /**
+ * A builder for {@link UserData} objects.
+ */
+ public static final class Builder {
+ private final String mId;
+ private final ArrayList<String> mCategoryIds;
+ private final ArrayList<String> mValues;
+ private String mDefaultAlgorithm;
+ private Bundle mDefaultArgs;
+
+ // Map of autofill field categories to fleid classification algorithms and args
+ private ArrayMap<String, String> mCategoryAlgorithms;
+ private ArrayMap<String, Bundle> mCategoryArgs;
+
+ private boolean mDestroyed;
+
+ // Non-persistent array used to limit the number of unique ids.
+ private final ArraySet<String> mUniqueCategoryIds;
+ // Non-persistent array used to ignore duplaicated value/category pairs.
+ private final ArraySet<String> mUniqueValueCategoryPairs;
+
+ /**
+ * Creates a new builder for the user data used for <a href="#FieldClassification">field
+ * classification</a>.
+ *
+ * <p>The user data must contain at least one pair of {@code value} -> {@code categoryId},
+ * and more pairs can be added through the {@link #add(String, String)} method. For example:
+ *
+ * <pre class="prettyprint">
+ * new UserData.Builder("v1", "Bart Simpson", "name")
+ * .add("[email protected]", "email")
+ * .add("[email protected]", "email")
+ * .build();
+ * </pre>
+ *
+ * @param id id used to identify the whole {@link UserData} object. This id is also returned
+ * by {@link AutofillManager#getUserDataId()}, which can be used to check if the
+ * {@link UserData} is up-to-date without fetching the whole object (through
+ * {@link AutofillManager#getUserData()}).
+ *
+ * @param value value of the user data.
+ * @param categoryId autofill field category.
+ *
+ * @throws IllegalArgumentException if any of the following occurs:
+ * <ul>
+ * <li>{@code id} is empty</li>
+ * <li>{@code categoryId} is empty</li>
+ * <li>{@code value} is empty</li>
+ * <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}</li>
+ * <li>the length of {@code value} is higher than
+ * {@link UserData#getMaxValueLength()}</li>
+ * </ul>
+ */
+ public Builder(@NonNull String id, @NonNull String value, @NonNull String categoryId) {
+ mId = checkNotEmpty("id", id);
+ checkNotEmpty("categoryId", categoryId);
+ checkValidValue(value);
+ final int maxUserDataSize = getMaxUserDataSize();
+ mCategoryIds = new ArrayList<>(maxUserDataSize);
+ mValues = new ArrayList<>(maxUserDataSize);
+ mUniqueValueCategoryPairs = new ArraySet<>(maxUserDataSize);
+
+ mUniqueCategoryIds = new ArraySet<>(getMaxCategoryCount());
+
+ addMapping(value, categoryId);
+ }
+
+ /**
+ * Sets the default algorithm used for
+ * <a href="#FieldClassification">field classification</a>.
+ *
+ * <p>The currently available algorithms can be retrieve through
+ * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
+ *
+ * <p>If not set, the
+ * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
+ * used instead.
+ *
+ * @param name name of the algorithm or {@code null} to used default.
+ * @param args optional arguments to the algorithm.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setFieldClassificationAlgorithm(@Nullable String name,
+ @Nullable Bundle args) {
+ throwIfDestroyed();
+ mDefaultAlgorithm = name;
+ mDefaultArgs = args;
+ return this;
+ }
+
+ /**
+ * Sets the algorithm used for <a href="#FieldClassification">field classification</a>
+ * for the specified category.
+ *
+ * <p>The currently available algorithms can be retrieved through
+ * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
+ *
+ * <p>If not set, the
+ * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
+ * used instead.
+ *
+ * @param categoryId autofill field category.
+ * @param name name of the algorithm or {@code null} to used default.
+ * @param args optional arguments to the algorithm.
+ *
+ * @return this builder
+ */
+ @NonNull
+ public Builder setFieldClassificationAlgorithmForCategory(@NonNull String categoryId,
+ @Nullable String name, @Nullable Bundle args) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(categoryId);
+ if (mCategoryAlgorithms == null) {
+ mCategoryAlgorithms = new ArrayMap<>(getMaxCategoryCount());
+ }
+ if (mCategoryArgs == null) {
+ mCategoryArgs = new ArrayMap<>(getMaxCategoryCount());
+ }
+ mCategoryAlgorithms.put(categoryId, name);
+ mCategoryArgs.put(categoryId, args);
+ return this;
+ }
+
+ /**
+ * Adds a new value for user data.
+ *
+ * @param value value of the user data.
+ * @param categoryId string used to identify the category the value is associated with.
+ *
+ * @throws IllegalStateException if:
+ * <ul>
+ * <li>{@link #build()} already called</li>
+ * <li>the {@code value} has already been added (<b>Note: </b> this restriction was
+ * lifted on Android {@link android.os.Build.VERSION_CODES#Q} and later)</li>
+ * <li>the number of unique {@code categoryId} values added so far is more than
+ * {@link UserData#getMaxCategoryCount()}</li>
+ * <li>the number of {@code values} added so far is is more than
+ * {@link UserData#getMaxUserDataSize()}</li>
+ * </ul>
+ *
+ * @throws IllegalArgumentException if any of the following occurs:
+ * <ul>
+ * <li>{@code id} is empty</li>
+ * <li>{@code categoryId} is empty</li>
+ * <li>{@code value} is empty</li>
+ * <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}</li>
+ * <li>the length of {@code value} is higher than
+ * {@link UserData#getMaxValueLength()}</li>
+ * </ul>
+ */
+ @NonNull
+ public Builder add(@NonNull String value, @NonNull String categoryId) {
+ throwIfDestroyed();
+ checkNotEmpty("categoryId", categoryId);
+ checkValidValue(value);
+
+ if (!mUniqueCategoryIds.contains(categoryId)) {
+ // New category - check size
+ Preconditions.checkState(mUniqueCategoryIds.size() < getMaxCategoryCount(),
+ "already added " + mUniqueCategoryIds.size() + " unique category ids");
+ }
+
+ Preconditions.checkState(mValues.size() < getMaxUserDataSize(),
+ "already added " + mValues.size() + " elements");
+ addMapping(value, categoryId);
+
+ return this;
+ }
+
+ private void addMapping(@NonNull String value, @NonNull String categoryId) {
+ final String pair = value + ":" + categoryId;
+ if (mUniqueValueCategoryPairs.contains(pair)) {
+ // Don't include value on message because it could contain PII
+ Log.w(TAG, "Ignoring entry with same value / category");
+ return;
+ }
+ mCategoryIds.add(categoryId);
+ mValues.add(value);
+ mUniqueCategoryIds.add(categoryId);
+ mUniqueValueCategoryPairs.add(pair);
+ }
+
+ private String checkNotEmpty(@NonNull String name, @Nullable String value) {
+ Preconditions.checkNotNull(value);
+ Preconditions.checkArgument(!TextUtils.isEmpty(value), "%s cannot be empty", name);
+ return value;
+ }
+
+ private void checkValidValue(@Nullable String value) {
+ Preconditions.checkNotNull(value);
+ final int length = value.length();
+ Preconditions.checkArgumentInRange(length, getMinValueLength(),
+ getMaxValueLength(), "value length (" + length + ")");
+ }
+
+ /**
+ * Creates a new {@link UserData} instance.
+ *
+ * <p>You should not interact with this builder once this method is called.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return The built dataset.
+ */
+ @NonNull
+ public UserData build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new UserData(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ final StringBuilder builder = new StringBuilder("UserData: [id=").append(mId);
+ // Cannot disclose category ids or values because they could contain PII
+ builder.append(", categoryIds=");
+ Helper.appendRedacted(builder, mCategoryIds);
+ builder.append(", values=");
+ Helper.appendRedacted(builder, mValues);
+ return builder.append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(mId);
+ parcel.writeStringArray(mCategoryIds);
+ parcel.writeStringArray(mValues);
+ parcel.writeString(mDefaultAlgorithm);
+ parcel.writeBundle(mDefaultArgs);
+ parcel.writeMap(mCategoryAlgorithms);
+ parcel.writeMap(mCategoryArgs);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<UserData> CREATOR =
+ new Parcelable.Creator<UserData>() {
+ @Override
+ public UserData createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final String id = parcel.readString();
+ final String[] categoryIds = parcel.readStringArray();
+ final String[] values = parcel.readStringArray();
+ final String defaultAlgorithm = parcel.readString();
+ final Bundle defaultArgs = parcel.readBundle();
+ final ArrayMap<String, String> categoryAlgorithms = new ArrayMap<>();
+ parcel.readMap(categoryAlgorithms, String.class.getClassLoader());
+ final ArrayMap<String, Bundle> categoryArgs = new ArrayMap<>();
+ parcel.readMap(categoryArgs, Bundle.class.getClassLoader());
+
+ final Builder builder = new Builder(id, values[0], categoryIds[0])
+ .setFieldClassificationAlgorithm(defaultAlgorithm, defaultArgs);
+
+ for (int i = 1; i < categoryIds.length; i++) {
+ String categoryId = categoryIds[i];
+ builder.add(values[i], categoryId);
+ }
+
+ final int size = categoryAlgorithms.size();
+ if (size > 0) {
+ for (int i = 0; i < size; i++) {
+ final String categoryId = categoryAlgorithms.keyAt(i);
+ builder.setFieldClassificationAlgorithmForCategory(categoryId,
+ categoryAlgorithms.valueAt(i), categoryArgs.get(categoryId));
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public UserData[] newArray(int size) {
+ return new UserData[size];
+ }
+ };
+
+ /**
+ * Gets the maximum number of values that can be added to a {@link UserData}.
+ */
+ public static int getMaxUserDataSize() {
+ return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE);
+ }
+
+ /**
+ * Gets the maximum number of ids that can be passed to {@link
+ * FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}.
+ */
+ public static int getMaxFieldClassificationIdsSize() {
+ return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
+ DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE);
+ }
+
+ /**
+ * Gets the maximum number of unique category ids that can be passed to
+ * the builder's constructor and {@link Builder#add(String, String)}.
+ */
+ public static int getMaxCategoryCount() {
+ return getInt(AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, DEFAULT_MAX_CATEGORY_COUNT);
+ }
+
+ /**
+ * Gets the minimum length of values passed to the builder's constructor or
+ * or {@link Builder#add(String, String)}.
+ */
+ public static int getMinValueLength() {
+ return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH);
+ }
+
+ /**
+ * Gets the maximum length of values passed to the builder's constructor or
+ * or {@link Builder#add(String, String)}.
+ */
+ public static int getMaxValueLength() {
+ return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH);
+ }
+
+ private static int getInt(String settings, int defaultValue) {
+ ContentResolver cr = null;
+ final ActivityThread at = ActivityThread.currentActivityThread();
+ if (at != null) {
+ cr = at.getApplication().getContentResolver();
+ }
+
+ if (cr == null) {
+ Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue);
+ return defaultValue;
+ }
+ return Settings.Secure.getInt(cr, settings, defaultValue);
+ }
+}
diff --git a/android/service/autofill/Validator.java b/android/service/autofill/Validator.java
new file mode 100644
index 0000000..a4036f2
--- /dev/null
+++ b/android/service/autofill/Validator.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+/**
+ * Class used to define whether a condition is satisfied.
+ *
+ * <p>Typically used to avoid displaying the save UI when the user input is invalid.
+ */
+public interface Validator {
+}
diff --git a/android/service/autofill/Validators.java b/android/service/autofill/Validators.java
new file mode 100644
index 0000000..0f1ba98
--- /dev/null
+++ b/android/service/autofill/Validators.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import android.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Factory for {@link Validator} operations.
+ *
+ * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples.
+ */
+public final class Validators {
+
+ private Validators() {
+ throw new UnsupportedOperationException("contains static methods only");
+ }
+
+ /**
+ * Creates a validator that is only valid if all {@code validators} are valid.
+ *
+ * <p>Used to represent an {@code AND} boolean operation in a chain of validators.
+ *
+ * @throws IllegalArgumentException if any element of {@code validators} is an instance of a
+ * class that is not provided by the Android System.
+ */
+ @NonNull
+ public static Validator and(@NonNull Validator...validators) {
+ return new RequiredValidators(getInternalValidators(validators));
+ }
+
+ /**
+ * Creates a validator that is valid if any of the {@code validators} is valid.
+ *
+ * <p>Used to represent an {@code OR} boolean operation in a chain of validators.
+ *
+ * @throws IllegalArgumentException if any element of {@code validators} is an instance of a
+ * class that is not provided by the Android System.
+ */
+ @NonNull
+ public static Validator or(@NonNull Validator...validators) {
+ return new OptionalValidators(getInternalValidators(validators));
+ }
+
+ /**
+ * Creates a validator that is valid when {@code validator} is not, and vice versa.
+ *
+ * <p>Used to represent a {@code NOT} boolean operation in a chain of validators.
+ *
+ * @throws IllegalArgumentException if {@code validator} is an instance of a class that is not
+ * provided by the Android System.
+ */
+ @NonNull
+ public static Validator not(@NonNull Validator validator) {
+ Preconditions.checkArgument(validator instanceof InternalValidator,
+ "validator not provided by Android System: " + validator);
+ return new NegationValidator((InternalValidator) validator);
+ }
+
+ private static InternalValidator[] getInternalValidators(Validator[] validators) {
+ Preconditions.checkArrayElementsNotNull(validators, "validators");
+
+ final InternalValidator[] internals = new InternalValidator[validators.length];
+
+ for (int i = 0; i < validators.length; i++) {
+ Preconditions.checkArgument((validators[i] instanceof InternalValidator),
+ "element " + i + " not provided by Android System: " + validators[i]);
+ internals[i] = (InternalValidator) validators[i];
+ }
+ return internals;
+ }
+}
diff --git a/android/service/autofill/ValueFinder.java b/android/service/autofill/ValueFinder.java
new file mode 100644
index 0000000..7f195d6
--- /dev/null
+++ b/android/service/autofill/ValueFinder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 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.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+
+/**
+ * Helper object used to obtain the value of a field in the screen being autofilled.
+ *
+ * @hide
+ */
+@TestApi
+public interface ValueFinder {
+
+ /**
+ * Gets the value of a field as String, or {@code null} when not found.
+ */
+ @Nullable
+ default String findByAutofillId(@NonNull AutofillId id) {
+ final AutofillValue value = findRawValueByAutofillId(id);
+ return (value == null || !value.isText()) ? null : value.getTextValue().toString();
+ }
+
+ /**
+ * Gets the value of a field, or {@code null} when not found.
+ */
+ @Nullable
+ AutofillValue findRawValueByAutofillId(@NonNull AutofillId id);
+}
diff --git a/android/service/autofill/VisibilitySetterAction.java b/android/service/autofill/VisibilitySetterAction.java
new file mode 100644
index 0000000..e29a23f
--- /dev/null
+++ b/android/service/autofill/VisibilitySetterAction.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2018 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.autofill;
+
+import static android.view.autofill.Helper.sDebug;
+import static android.view.autofill.Helper.sVerbose;
+
+import android.annotation.IdRes;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Slog;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.View.Visibility;
+import android.view.ViewGroup;
+import android.widget.RemoteViews;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Action used to change the visibility of other child view in a {@link CustomDescription}
+ * {@link RemoteViews presentation template}.
+ *
+ * <p>See {@link CustomDescription.Builder#addOnClickAction(int, OnClickAction)} for more details.
+ */
+public final class VisibilitySetterAction extends InternalOnClickAction implements
+ OnClickAction, Parcelable {
+ private static final String TAG = "VisibilitySetterAction";
+
+ @NonNull private final SparseIntArray mVisibilities;
+
+ private VisibilitySetterAction(@NonNull Builder builder) {
+ mVisibilities = builder.mVisibilities;
+ }
+
+ /** @hide */
+ @Override
+ public void onClick(@NonNull ViewGroup rootView) {
+ for (int i = 0; i < mVisibilities.size(); i++) {
+ final int id = mVisibilities.keyAt(i);
+ final View child = rootView.findViewById(id);
+ if (child == null) {
+ Slog.w(TAG, "Skipping view id " + id + " because it's not found on " + rootView);
+ continue;
+ }
+ final int visibility = mVisibilities.valueAt(i);
+ if (sVerbose) {
+ Slog.v(TAG, "Changing visibility of view " + child + " from "
+ + child.getVisibility() + " to " + visibility);
+ }
+ child.setVisibility(visibility);
+ }
+ }
+
+ /**
+ * Builder for {@link VisibilitySetterAction} objects.
+ */
+ public static final class Builder {
+ private final SparseIntArray mVisibilities = new SparseIntArray();
+ private boolean mDestroyed;
+
+ /**
+ * Creates a new builder for an action that change the visibility of one child view.
+ *
+ * @param id view resource id of the children view.
+ * @param visibility one of {@link View#VISIBLE}, {@link View#INVISIBLE}, or
+ * {@link View#GONE}.
+ * @throws IllegalArgumentException if visibility is not one of {@link View#VISIBLE},
+ * {@link View#INVISIBLE}, or {@link View#GONE}.
+ */
+ public Builder(@IdRes int id, @Visibility int visibility) {
+ setVisibility(id, visibility);
+ }
+
+ /**
+ * Sets the action to changes the visibility of a child view.
+ *
+ * @param id view resource id of the children view.
+ * @param visibility one of {@link View#VISIBLE}, {@link View#INVISIBLE}, or
+ * {@link View#GONE}.
+ * @throws IllegalArgumentException if visibility is not one of {@link View#VISIBLE},
+ * {@link View#INVISIBLE}, or {@link View#GONE}.
+ */
+ @NonNull
+ public Builder setVisibility(@IdRes int id, @Visibility int visibility) {
+ throwIfDestroyed();
+ switch (visibility) {
+ case View.VISIBLE:
+ case View.INVISIBLE:
+ case View.GONE:
+ mVisibilities.put(id, visibility);
+ return this;
+ }
+ throw new IllegalArgumentException("Invalid visibility: " + visibility);
+ }
+
+ /**
+ * Creates a new {@link VisibilitySetterAction} instance.
+ */
+ @NonNull
+ public VisibilitySetterAction build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new VisibilitySetterAction(this);
+ }
+
+ private void throwIfDestroyed() {
+ Preconditions.checkState(!mDestroyed, "Already called build()");
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ return "VisibilitySetterAction: [" + mVisibilities + "]";
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeSparseIntArray(mVisibilities);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<VisibilitySetterAction> CREATOR =
+ new Parcelable.Creator<VisibilitySetterAction>() {
+
+ @NonNull
+ @Override
+ public VisibilitySetterAction createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ final SparseIntArray visibilities = parcel.readSparseIntArray();
+ Builder builder = null;
+ for (int i = 0; i < visibilities.size(); i++) {
+ final int id = visibilities.keyAt(i);
+ final int visibility = visibilities.valueAt(i);
+ if (builder == null) {
+ builder = new Builder(id, visibility);
+ } else {
+ builder.setVisibility(id, visibility);
+ }
+ }
+ return builder == null ? null : builder.build();
+ }
+
+ @NonNull
+ @Override
+ public VisibilitySetterAction[] newArray(int size) {
+ return new VisibilitySetterAction[size];
+ }
+ };
+}
diff --git a/android/service/autofill/augmented/AugmentedAutofillService.java b/android/service/autofill/augmented/AugmentedAutofillService.java
new file mode 100644
index 0000000..cca45f5
--- /dev/null
+++ b/android/service/autofill/augmented/AugmentedAutofillService.java
@@ -0,0 +1,677 @@
+/*
+ * Copyright (C) 2018 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.autofill.augmented;
+
+import static android.service.autofill.augmented.Helper.logResponse;
+import static android.util.TimeUtils.formatDuration;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.BaseBundle;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillEventHistory;
+import android.service.autofill.augmented.PresentationParams.SystemPopupPresentationParams;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+import android.view.autofill.IAugmentedAutofillManagerClient;
+import android.view.autofill.IAutofillWindowPresenter;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A service used to augment the Autofill subsystem by potentially providing autofill data when the
+ * "standard" workflow failed (for example, because the standard AutofillService didn't have data).
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public abstract class AugmentedAutofillService extends Service {
+
+ private static final String TAG = AugmentedAutofillService.class.getSimpleName();
+
+ static boolean sDebug = Build.IS_USER ? false : true;
+ static boolean sVerbose = false;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_AUGMENTED_AUTOFILL_SERVICE} permission so
+ * that other applications can not abuse it.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.autofill.augmented.AugmentedAutofillService";
+
+ private Handler mHandler;
+
+ private SparseArray<AutofillProxy> mAutofillProxies;
+
+ private AutofillProxy mAutofillProxyForLastRequest;
+
+ // Used for metrics / debug only
+ private ComponentName mServiceComponentName;
+
+ private final class AugmentedAutofillServiceImpl extends IAugmentedAutofillService.Stub {
+
+ @Override
+ public void onConnected(boolean debug, boolean verbose) {
+ mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnConnected,
+ AugmentedAutofillService.this, debug, verbose));
+ }
+
+ @Override
+ public void onDisconnected() {
+ mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnDisconnected,
+ AugmentedAutofillService.this));
+ }
+
+ @Override
+ public void onFillRequest(int sessionId, IBinder client, int taskId,
+ ComponentName componentName, AutofillId focusedId, AutofillValue focusedValue,
+ long requestTime, @Nullable InlineSuggestionsRequest inlineSuggestionsRequest,
+ IFillCallback callback) {
+ mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnFillRequest,
+ AugmentedAutofillService.this, sessionId, client, taskId, componentName,
+ focusedId, focusedValue, requestTime, inlineSuggestionsRequest, callback));
+ }
+
+ @Override
+ public void onDestroyAllFillWindowsRequest() {
+ mHandler.sendMessage(
+ obtainMessage(AugmentedAutofillService::handleOnDestroyAllFillWindowsRequest,
+ AugmentedAutofillService.this));
+ }
+ };
+
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler(Looper.getMainLooper(), null, true);
+ BaseBundle.setShouldDefuse(true);
+ }
+
+ /** @hide */
+ @Override
+ public final IBinder onBind(Intent intent) {
+ mServiceComponentName = intent.getComponent();
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return new AugmentedAutofillServiceImpl();
+ }
+ Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+ return null;
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnUnbind,
+ AugmentedAutofillService.this));
+ return false;
+ }
+
+ /**
+ * Called when the Android system connects to service.
+ *
+ * <p>You should generally do initialization here rather than in {@link #onCreate}.
+ */
+ public void onConnected() {
+ }
+
+ /**
+ * The child class of the service can call this method to initiate an Autofill flow.
+ *
+ * <p> The request would be respected only if the previous augmented autofill request was
+ * made for the same {@code activityComponent} and {@code autofillId}, and the field is
+ * currently on focus.
+ *
+ * <p> The request would start a new autofill flow. It doesn't guarantee that the
+ * {@link AutofillManager} will proceed with the request.
+ *
+ * @param activityComponent the client component for which the autofill is requested for
+ * @param autofillId the client field id for which the autofill is requested for
+ * @return true if the request makes the {@link AutofillManager} start a new Autofill flow,
+ * false otherwise.
+ */
+ public final boolean requestAutofill(@NonNull ComponentName activityComponent,
+ @NonNull AutofillId autofillId) {
+ // TODO(b/149531989): revisit this. The request should start a new autofill session
+ // rather than reusing the existing session.
+ final AutofillProxy proxy = mAutofillProxyForLastRequest;
+ if (proxy == null || !proxy.mComponentName.equals(activityComponent)
+ || !proxy.mFocusedId.equals(autofillId)) {
+ return false;
+ }
+ try {
+ return proxy.requestAutofill();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return false;
+ }
+
+ /**
+ * Asks the service to handle an "augmented" autofill request.
+ *
+ * <p>This method is called when the "stantard" autofill service cannot handle a request, which
+ * typically occurs when:
+ * <ul>
+ * <li>Service does not recognize what should be autofilled.
+ * <li>Service does not have data to fill the request.
+ * <li>Service blacklisted that app (or activity) for autofill.
+ * <li>App disabled itself for autofill.
+ * </ul>
+ *
+ * <p>Differently from the standard autofill workflow, on augmented autofill the service is
+ * responsible to generate the autofill UI and request the Android system to autofill the
+ * activity when the user taps an action in that UI (through the
+ * {@link FillController#autofill(List)} method).
+ *
+ * <p>The service <b>MUST</b> call {@link
+ * FillCallback#onSuccess(android.service.autofill.augmented.FillResponse)} as soon as possible,
+ * passing {@code null} when it cannot fulfill the request.
+ * @param request the request to handle.
+ * @param cancellationSignal signal for observing cancellation requests. The system will use
+ * this to notify you that the fill result is no longer needed and you should stop
+ * handling this fill request in order to save resources.
+ * @param controller object used to interact with the autofill system.
+ * @param callback object used to notify the result of the request. Service <b>must</b> call
+ * {@link FillCallback#onSuccess(android.service.autofill.augmented.FillResponse)}.
+ */
+ public void onFillRequest(@NonNull FillRequest request,
+ @NonNull CancellationSignal cancellationSignal, @NonNull FillController controller,
+ @NonNull FillCallback callback) {
+ }
+
+ /**
+ * Called when the Android system disconnects from the service.
+ *
+ * <p> At this point this service may no longer be an active {@link AugmentedAutofillService}.
+ * It should not make calls on {@link AutofillManager} that requires the caller to be
+ * the current service.
+ */
+ public void onDisconnected() {
+ }
+
+ private void handleOnConnected(boolean debug, boolean verbose) {
+ if (sDebug || debug) {
+ Log.d(TAG, "handleOnConnected(): debug=" + debug + ", verbose=" + verbose);
+ }
+ sDebug = debug;
+ sVerbose = verbose;
+ onConnected();
+ }
+
+ private void handleOnDisconnected() {
+ onDisconnected();
+ }
+
+ private void handleOnFillRequest(int sessionId, @NonNull IBinder client, int taskId,
+ @NonNull ComponentName componentName, @NonNull AutofillId focusedId,
+ @Nullable AutofillValue focusedValue, long requestTime,
+ @Nullable InlineSuggestionsRequest inlineSuggestionsRequest,
+ @NonNull IFillCallback callback) {
+ if (mAutofillProxies == null) {
+ mAutofillProxies = new SparseArray<>();
+ }
+
+ final ICancellationSignal transport = CancellationSignal.createTransport();
+ final CancellationSignal cancellationSignal = CancellationSignal.fromTransport(transport);
+ AutofillProxy proxy = mAutofillProxies.get(sessionId);
+ if (proxy == null) {
+ proxy = new AutofillProxy(sessionId, client, taskId, mServiceComponentName,
+ componentName, focusedId, focusedValue, requestTime, callback,
+ cancellationSignal);
+ mAutofillProxies.put(sessionId, proxy);
+ } else {
+ // TODO(b/123099468): figure out if it's ok to reuse the proxy; add logging
+ if (sDebug) Log.d(TAG, "Reusing proxy for session " + sessionId);
+ proxy.update(focusedId, focusedValue, callback, cancellationSignal);
+ }
+
+ try {
+ callback.onCancellable(transport);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ mAutofillProxyForLastRequest = proxy;
+ onFillRequest(new FillRequest(proxy, inlineSuggestionsRequest), cancellationSignal,
+ new FillController(proxy), new FillCallback(proxy));
+ }
+
+ private void handleOnDestroyAllFillWindowsRequest() {
+ if (mAutofillProxies != null) {
+ final int size = mAutofillProxies.size();
+ for (int i = 0; i < size; i++) {
+ final int sessionId = mAutofillProxies.keyAt(i);
+ final AutofillProxy proxy = mAutofillProxies.valueAt(i);
+ if (proxy == null) {
+ // TODO(b/123100811): this might be fine, in which case we should logv it
+ Log.w(TAG, "No proxy for session " + sessionId);
+ return;
+ }
+ if (proxy.mCallback != null) {
+ try {
+ if (!proxy.mCallback.isCompleted()) {
+ proxy.mCallback.cancel();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "failed to check current pending request status", e);
+ }
+ }
+ proxy.destroy();
+ }
+ mAutofillProxies.clear();
+ mAutofillProxyForLastRequest = null;
+ }
+ }
+
+ private void handleOnUnbind() {
+ if (mAutofillProxies == null) {
+ if (sDebug) Log.d(TAG, "onUnbind(): no proxy to destroy");
+ return;
+ }
+ final int size = mAutofillProxies.size();
+ if (sDebug) Log.d(TAG, "onUnbind(): destroying " + size + " proxies");
+ for (int i = 0; i < size; i++) {
+ final AutofillProxy proxy = mAutofillProxies.valueAt(i);
+ try {
+ proxy.destroy();
+ } catch (Exception e) {
+ Log.w(TAG, "error destroying " + proxy);
+ }
+ }
+ mAutofillProxies = null;
+ mAutofillProxyForLastRequest = null;
+ }
+
+ @Override
+ /** @hide */
+ protected final void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.print("Service component: "); pw.println(
+ ComponentName.flattenToShortString(mServiceComponentName));
+ if (mAutofillProxies != null) {
+ final int size = mAutofillProxies.size();
+ pw.print("Number proxies: "); pw.println(size);
+ for (int i = 0; i < size; i++) {
+ final int sessionId = mAutofillProxies.keyAt(i);
+ final AutofillProxy proxy = mAutofillProxies.valueAt(i);
+ pw.print(i); pw.print(") SessionId="); pw.print(sessionId); pw.println(":");
+ proxy.dump(" ", pw);
+ }
+ }
+ dump(pw, args);
+ }
+
+ /**
+ * Implementation specific {@code dump}. The child class can override the method to provide
+ * additional information about the Service's state into the dumpsys output.
+ *
+ * @param pw The PrintWriter to which you should dump your state. This will be closed for
+ * you after you return.
+ * @param args additional arguments to the dump request.
+ */
+ protected void dump(@NonNull PrintWriter pw,
+ @SuppressWarnings("unused") @NonNull String[] args) {
+ pw.print(getClass().getName()); pw.println(": nothing to dump");
+ }
+
+ /**
+ * Gets the inline augmented autofill events that happened after the last
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillController, FillCallback)} call.
+ *
+ * <p>The history is not persisted over reboots, and it's cleared every time the service
+ * replies to a
+ * {@link #onFillRequest(FillRequest, CancellationSignal, FillController, FillCallback)}
+ * by calling {@link FillCallback#onSuccess(FillResponse)}. Hence, the service should call
+ * {@link #getFillEventHistory() before finishing the {@link FillCallback}.
+ *
+ * <p>Also note that the events from the dropdown suggestion UI is not stored in the history
+ * since the service owns the UI.
+ *
+ * @return The history or {@code null} if there are no events.
+ */
+ @Nullable public final FillEventHistory getFillEventHistory() {
+ final AutofillManager afm = getSystemService(AutofillManager.class);
+
+ if (afm == null) {
+ return null;
+ } else {
+ return afm.getFillEventHistory();
+ }
+ }
+
+ /** @hide */
+ static final class AutofillProxy {
+
+ static final int REPORT_EVENT_NO_RESPONSE = 1;
+ static final int REPORT_EVENT_UI_SHOWN = 2;
+ static final int REPORT_EVENT_UI_DESTROYED = 3;
+ static final int REPORT_EVENT_INLINE_RESPONSE = 4;
+
+ @IntDef(prefix = { "REPORT_EVENT_" }, value = {
+ REPORT_EVENT_NO_RESPONSE,
+ REPORT_EVENT_UI_SHOWN,
+ REPORT_EVENT_UI_DESTROYED,
+ REPORT_EVENT_INLINE_RESPONSE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ReportEvent{}
+
+
+ private final Object mLock = new Object();
+ private final IAugmentedAutofillManagerClient mClient;
+ private final int mSessionId;
+ public final int mTaskId;
+ public final ComponentName mComponentName;
+ // Used for metrics / debug only
+ private String mServicePackageName;
+ @GuardedBy("mLock")
+ private AutofillId mFocusedId;
+ @GuardedBy("mLock")
+ private AutofillValue mFocusedValue;
+ @GuardedBy("mLock")
+ private IFillCallback mCallback;
+
+ /**
+ * Id of the last field that cause the Autofill UI to be shown.
+ *
+ * <p>Used to make sure the SmartSuggestionsParams is updated when a new fields is focused.
+ */
+ @GuardedBy("mLock")
+ private AutofillId mLastShownId;
+
+ // Objects used to log metrics
+ private final long mFirstRequestTime;
+ private long mFirstOnSuccessTime;
+ private long mUiFirstShownTime;
+ private long mUiFirstDestroyedTime;
+
+ @GuardedBy("mLock")
+ private SystemPopupPresentationParams mSmartSuggestion;
+
+ @GuardedBy("mLock")
+ private FillWindow mFillWindow;
+
+ private CancellationSignal mCancellationSignal;
+
+ private AutofillProxy(int sessionId, @NonNull IBinder client, int taskId,
+ @NonNull ComponentName serviceComponentName,
+ @NonNull ComponentName componentName, @NonNull AutofillId focusedId,
+ @Nullable AutofillValue focusedValue, long requestTime,
+ @NonNull IFillCallback callback, @NonNull CancellationSignal cancellationSignal) {
+ mSessionId = sessionId;
+ mClient = IAugmentedAutofillManagerClient.Stub.asInterface(client);
+ mCallback = callback;
+ mTaskId = taskId;
+ mComponentName = componentName;
+ mServicePackageName = serviceComponentName.getPackageName();
+ mFocusedId = focusedId;
+ mFocusedValue = focusedValue;
+ mFirstRequestTime = requestTime;
+ mCancellationSignal = cancellationSignal;
+ // TODO(b/123099468): linkToDeath
+ }
+
+ @NonNull
+ public SystemPopupPresentationParams getSmartSuggestionParams() {
+ synchronized (mLock) {
+ if (mSmartSuggestion != null && mFocusedId.equals(mLastShownId)) {
+ return mSmartSuggestion;
+ }
+ Rect rect;
+ try {
+ rect = mClient.getViewCoordinates(mFocusedId);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Could not get coordinates for " + mFocusedId);
+ return null;
+ }
+ if (rect == null) {
+ if (sDebug) Log.d(TAG, "getViewCoordinates(" + mFocusedId + ") returned null");
+ return null;
+ }
+ mSmartSuggestion = new SystemPopupPresentationParams(this, rect);
+ mLastShownId = mFocusedId;
+ return mSmartSuggestion;
+ }
+ }
+
+ public void autofill(@NonNull List<Pair<AutofillId, AutofillValue>> pairs)
+ throws RemoteException {
+ final int size = pairs.size();
+ final List<AutofillId> ids = new ArrayList<>(size);
+ final List<AutofillValue> values = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ final Pair<AutofillId, AutofillValue> pair = pairs.get(i);
+ ids.add(pair.first);
+ values.add(pair.second);
+ }
+ final boolean hideHighlight = size == 1 && ids.get(0).equals(mFocusedId);
+ mClient.autofill(mSessionId, ids, values, hideHighlight);
+ }
+
+ public void setFillWindow(@NonNull FillWindow fillWindow) {
+ synchronized (mLock) {
+ mFillWindow = fillWindow;
+ }
+ }
+
+ public FillWindow getFillWindow() {
+ synchronized (mLock) {
+ return mFillWindow;
+ }
+ }
+
+ public void requestShowFillUi(int width, int height, Rect anchorBounds,
+ IAutofillWindowPresenter presenter) throws RemoteException {
+ if (mCancellationSignal.isCanceled()) {
+ if (sVerbose) {
+ Log.v(TAG, "requestShowFillUi() not showing because request is cancelled");
+ }
+ return;
+ }
+ mClient.requestShowFillUi(mSessionId, mFocusedId, width, height, anchorBounds,
+ presenter);
+ }
+
+ public void requestHideFillUi() throws RemoteException {
+ mClient.requestHideFillUi(mSessionId, mFocusedId);
+ }
+
+
+ private boolean requestAutofill() throws RemoteException {
+ return mClient.requestAutofill(mSessionId, mFocusedId);
+ }
+
+ private void update(@NonNull AutofillId focusedId, @NonNull AutofillValue focusedValue,
+ @NonNull IFillCallback callback, @NonNull CancellationSignal cancellationSignal) {
+ synchronized (mLock) {
+ mFocusedId = focusedId;
+ mFocusedValue = focusedValue;
+ if (mCallback != null) {
+ try {
+ if (!mCallback.isCompleted()) {
+ mCallback.cancel();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to check current pending request status", e);
+ }
+ Log.d(TAG, "mCallback is updated.");
+ }
+ mCallback = callback;
+ mCancellationSignal = cancellationSignal;
+ }
+ }
+
+ @NonNull
+ public AutofillId getFocusedId() {
+ synchronized (mLock) {
+ return mFocusedId;
+ }
+ }
+
+ @NonNull
+ public AutofillValue getFocusedValue() {
+ synchronized (mLock) {
+ return mFocusedValue;
+ }
+ }
+
+ void reportResult(@Nullable List<Dataset> inlineSuggestionsData,
+ @Nullable Bundle clientState) {
+ try {
+ mCallback.onSuccess(inlineSuggestionsData, clientState);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling back with the inline suggestions data: " + e);
+ }
+ }
+
+ void logEvent(@ReportEvent int event) {
+ if (sVerbose) Log.v(TAG, "returnAndLogResult(): " + event);
+ long duration = -1;
+ int type = MetricsEvent.TYPE_UNKNOWN;
+
+ switch (event) {
+ case REPORT_EVENT_NO_RESPONSE: {
+ type = MetricsEvent.TYPE_SUCCESS;
+ if (mFirstOnSuccessTime == 0) {
+ mFirstOnSuccessTime = SystemClock.elapsedRealtime();
+ duration = mFirstOnSuccessTime - mFirstRequestTime;
+ if (sDebug) {
+ Log.d(TAG, "Service responded nothing in " + formatDuration(duration));
+ }
+ }
+ } break;
+
+ case REPORT_EVENT_INLINE_RESPONSE: {
+ // TODO: Define a constant and log this event
+ // type = MetricsEvent.TYPE_SUCCESS_INLINE;
+ if (mFirstOnSuccessTime == 0) {
+ mFirstOnSuccessTime = SystemClock.elapsedRealtime();
+ duration = mFirstOnSuccessTime - mFirstRequestTime;
+ if (sDebug) {
+ Log.d(TAG, "Service responded nothing in " + formatDuration(duration));
+ }
+ }
+ } break;
+
+ case REPORT_EVENT_UI_SHOWN: {
+ type = MetricsEvent.TYPE_OPEN;
+ if (mUiFirstShownTime == 0) {
+ mUiFirstShownTime = SystemClock.elapsedRealtime();
+ duration = mUiFirstShownTime - mFirstRequestTime;
+ if (sDebug) Log.d(TAG, "UI shown in " + formatDuration(duration));
+ }
+ } break;
+
+ case REPORT_EVENT_UI_DESTROYED: {
+ type = MetricsEvent.TYPE_CLOSE;
+ if (mUiFirstDestroyedTime == 0) {
+ mUiFirstDestroyedTime = SystemClock.elapsedRealtime();
+ duration = mUiFirstDestroyedTime - mFirstRequestTime;
+ if (sDebug) Log.d(TAG, "UI destroyed in " + formatDuration(duration));
+ }
+ } break;
+
+ default:
+ Log.w(TAG, "invalid event reported: " + event);
+ }
+ logResponse(type, mServicePackageName, mComponentName, mSessionId, duration);
+ }
+
+ public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ pw.print(prefix); pw.print("sessionId: "); pw.println(mSessionId);
+ pw.print(prefix); pw.print("taskId: "); pw.println(mTaskId);
+ pw.print(prefix); pw.print("component: ");
+ pw.println(mComponentName.flattenToShortString());
+ pw.print(prefix); pw.print("focusedId: "); pw.println(mFocusedId);
+ if (mFocusedValue != null) {
+ pw.print(prefix); pw.print("focusedValue: "); pw.println(mFocusedValue);
+ }
+ if (mLastShownId != null) {
+ pw.print(prefix); pw.print("lastShownId: "); pw.println(mLastShownId);
+ }
+ pw.print(prefix); pw.print("client: "); pw.println(mClient);
+ final String prefix2 = prefix + " ";
+ if (mFillWindow != null) {
+ pw.print(prefix); pw.println("window:");
+ mFillWindow.dump(prefix2, pw);
+ }
+ if (mSmartSuggestion != null) {
+ pw.print(prefix); pw.println("smartSuggestion:");
+ mSmartSuggestion.dump(prefix2, pw);
+ }
+ if (mFirstOnSuccessTime > 0) {
+ final long responseTime = mFirstOnSuccessTime - mFirstRequestTime;
+ pw.print(prefix); pw.print("response time: ");
+ formatDuration(responseTime, pw); pw.println();
+ }
+
+ if (mUiFirstShownTime > 0) {
+ final long uiRenderingTime = mUiFirstShownTime - mFirstRequestTime;
+ pw.print(prefix); pw.print("UI rendering time: ");
+ formatDuration(uiRenderingTime, pw); pw.println();
+ }
+
+ if (mUiFirstDestroyedTime > 0) {
+ final long uiTotalTime = mUiFirstDestroyedTime - mFirstRequestTime;
+ pw.print(prefix); pw.print("UI life time: ");
+ formatDuration(uiTotalTime, pw); pw.println();
+ }
+ }
+
+ private void destroy() {
+ synchronized (mLock) {
+ if (mFillWindow != null) {
+ if (sDebug) Log.d(TAG, "destroying window");
+ mFillWindow.destroy();
+ mFillWindow = null;
+ }
+ }
+ }
+ }
+}
diff --git a/android/service/autofill/augmented/FillCallback.java b/android/service/autofill/augmented/FillCallback.java
new file mode 100644
index 0000000..21738d8
--- /dev/null
+++ b/android/service/autofill/augmented/FillCallback.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 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.autofill.augmented;
+
+import static android.service.autofill.augmented.AugmentedAutofillService.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Bundle;
+import android.service.autofill.Dataset;
+import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Callback used to indicate at {@link FillRequest} has been fulfilled.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class FillCallback {
+
+ private static final String TAG = FillCallback.class.getSimpleName();
+
+ private final AutofillProxy mProxy;
+
+ FillCallback(@NonNull AutofillProxy proxy) {
+ mProxy = proxy;
+ }
+
+ /**
+ * Sets the response associated with the request.
+ *
+ * @param response response associated with the request, or {@code null} if the service
+ * could not provide autofill for the request.
+ */
+ public void onSuccess(@Nullable FillResponse response) {
+ if (sDebug) Log.d(TAG, "onSuccess(): " + response);
+
+ if (response == null) {
+ mProxy.logEvent(AutofillProxy.REPORT_EVENT_NO_RESPONSE);
+ mProxy.reportResult(/* inlineSuggestionsData */ null, /* clientState */ null);
+ return;
+ }
+
+ List<Dataset> inlineSuggestions = response.getInlineSuggestions();
+ Bundle clientState = response.getClientState();
+ if (inlineSuggestions != null && !inlineSuggestions.isEmpty()) {
+ mProxy.logEvent(AutofillProxy.REPORT_EVENT_INLINE_RESPONSE);
+ mProxy.reportResult(inlineSuggestions, clientState);
+ return;
+ }
+
+ final FillWindow fillWindow = response.getFillWindow();
+ if (fillWindow != null) {
+ fillWindow.show();
+ }
+ // TODO(b/123099468): must notify the server so it can update the session state to avoid
+ // showing conflicting UIs (for example, if a new request is made to the main autofill
+ // service and it now wants to show something).
+ }
+}
diff --git a/android/service/autofill/augmented/FillController.java b/android/service/autofill/augmented/FillController.java
new file mode 100644
index 0000000..7d552d6
--- /dev/null
+++ b/android/service/autofill/augmented/FillController.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 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.autofill.augmented;
+
+import static android.service.autofill.augmented.AugmentedAutofillService.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.RemoteException;
+import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
+import android.util.Log;
+import android.util.Pair;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * Object used to interact with the autofill system.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class FillController {
+ private static final String TAG = FillController.class.getSimpleName();
+
+ private final AutofillProxy mProxy;
+
+ FillController(@NonNull AutofillProxy proxy) {
+ mProxy = proxy;
+ }
+
+ /**
+ * Fills the activity with the provided values.
+ *
+ * <p>As a side effect, the {@link FillWindow} associated with the {@link FillResponse} will be
+ * automatically {@link FillWindow#destroy() destroyed}.
+ */
+ public void autofill(@NonNull List<Pair<AutofillId, AutofillValue>> values) {
+ Preconditions.checkNotNull(values);
+
+ if (sDebug) {
+ Log.d(TAG, "autofill() with " + values.size() + " values");
+ }
+
+ try {
+ mProxy.autofill(values);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+
+ final FillWindow fillWindow = mProxy.getFillWindow();
+ if (fillWindow != null) {
+ fillWindow.destroy();
+ }
+ }
+}
diff --git a/android/service/autofill/augmented/FillRequest.java b/android/service/autofill/augmented/FillRequest.java
new file mode 100644
index 0000000..6927cf6
--- /dev/null
+++ b/android/service/autofill/augmented/FillRequest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2018 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.autofill.augmented;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.ComponentName;
+import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.view.inputmethod.InlineSuggestionsRequest;
+
+import com.android.internal.util.DataClass;
+
+/**
+ * Represents a request to augment-fill an activity.
+ * @hide
+ */
+@SystemApi
+// TODO(b/123100811): pass a requestId and/or sessionId?
+@TestApi
+@DataClass(
+ genToString = true,
+ genBuilder = false,
+ genHiddenConstructor = true)
[email protected]({"getProxy"})
+public final class FillRequest {
+
+ private final @NonNull AutofillProxy mProxy;
+
+ //TODO(b/146901891): add detailed docs once we have stable APIs.
+ /**
+ * An optional request for inline suggestions.
+ */
+ private final @Nullable InlineSuggestionsRequest mInlineSuggestionsRequest;
+
+ /**
+ * Gets the task of the activity associated with this request.
+ */
+ public int getTaskId() {
+ return mProxy.mTaskId;
+ }
+
+ /**
+ * Gets the name of the activity associated with this request.
+ */
+ @NonNull
+ public ComponentName getActivityComponent() {
+ return mProxy.mComponentName;
+ }
+
+ /**
+ * Gets the id of the field that triggered the request.
+ */
+ @NonNull
+ public AutofillId getFocusedId() {
+ return mProxy.getFocusedId();
+ }
+
+ /**
+ * Gets the current value of the field that triggered the request.
+ */
+ @NonNull
+ public AutofillValue getFocusedValue() {
+ return mProxy.getFocusedValue();
+ }
+
+ /**
+ * Gets the Smart Suggestions object used to embed the autofill UI.
+ *
+ * @return object used to embed the autofill UI, or {@code null} if not supported.
+ */
+ @Nullable
+ public PresentationParams getPresentationParams() {
+ return mProxy.getSmartSuggestionParams();
+ }
+
+ String proxyToString() {
+ return "FillRequest[act=" + getActivityComponent().flattenToShortString()
+ + ", id=" + mProxy.getFocusedId() + "]";
+ }
+
+
+
+
+ // Code below generated by codegen v1.0.14.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/augmented/FillRequest.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new FillRequest.
+ *
+ * @param inlineSuggestionsRequest
+ * An optional request for inline suggestions.
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public FillRequest(
+ @NonNull AutofillProxy proxy,
+ @Nullable InlineSuggestionsRequest inlineSuggestionsRequest) {
+ this.mProxy = proxy;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mProxy);
+ this.mInlineSuggestionsRequest = inlineSuggestionsRequest;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * An optional request for inline suggestions.
+ */
+ @DataClass.Generated.Member
+ public @Nullable InlineSuggestionsRequest getInlineSuggestionsRequest() {
+ return mInlineSuggestionsRequest;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public String toString() {
+ // You can override field toString logic by defining methods like:
+ // String fieldNameToString() { ... }
+
+ return "FillRequest { " +
+ "proxy = " + proxyToString() + ", " +
+ "inlineSuggestionsRequest = " + mInlineSuggestionsRequest +
+ " }";
+ }
+
+ @DataClass.Generated(
+ time = 1577399314707L,
+ codegenVersion = "1.0.14",
+ sourceFile = "frameworks/base/core/java/android/service/autofill/augmented/FillRequest.java",
+ inputSignatures = "private final @android.annotation.NonNull android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy mProxy\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\npublic int getTaskId()\npublic @android.annotation.NonNull android.content.ComponentName getActivityComponent()\npublic @android.annotation.NonNull android.view.autofill.AutofillId getFocusedId()\npublic @android.annotation.NonNull android.view.autofill.AutofillValue getFocusedValue()\npublic @android.annotation.Nullable android.service.autofill.augmented.PresentationParams getPresentationParams()\n java.lang.String proxyToString()\nclass FillRequest extends java.lang.Object implements []\[email protected](genToString=true, genBuilder=false, genHiddenConstructor=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/service/autofill/augmented/FillResponse.java b/android/service/autofill/augmented/FillResponse.java
new file mode 100644
index 0000000..f72eb78
--- /dev/null
+++ b/android/service/autofill/augmented/FillResponse.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2018 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.autofill.augmented;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Bundle;
+import android.service.autofill.Dataset;
+
+import com.android.internal.util.DataClass;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Response to a {@link FillRequest}.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+@DataClass(
+ genBuilder = true,
+ genHiddenGetters = true)
+public final class FillResponse {
+
+ /**
+ * The {@link FillWindow} used to display the Autofill UI.
+ */
+ private @Nullable FillWindow mFillWindow;
+
+ /**
+ * The {@link Dataset}s representing the inline suggestions data. Defaults to null if no
+ * inline suggestions are available from the service.
+ */
+ @DataClass.PluralOf("inlineSuggestion")
+ private @Nullable List<Dataset> mInlineSuggestions;
+
+ /**
+ * The client state that {@link AugmentedAutofillService} implementation can put anything in to
+ * identify the request and the response when calling
+ * {@link AugmentedAutofillService#getFillEventHistory()}.
+ */
+ private @Nullable Bundle mClientState;
+
+ private static FillWindow defaultFillWindow() {
+ return null;
+ }
+
+ private static List<Dataset> defaultInlineSuggestions() {
+ return null;
+ }
+
+ private static Bundle defaultClientState() {
+ return null;
+ }
+
+
+ /** @hide */
+ abstract static class BaseBuilder {
+ abstract FillResponse.Builder addInlineSuggestion(@NonNull Dataset value);
+ }
+
+
+
+ // Code below generated by codegen v1.0.15.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/service/autofill/augmented/FillResponse.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ @DataClass.Generated.Member
+ /* package-private */ FillResponse(
+ @Nullable FillWindow fillWindow,
+ @Nullable List<Dataset> inlineSuggestions,
+ @Nullable Bundle clientState) {
+ this.mFillWindow = fillWindow;
+ this.mInlineSuggestions = inlineSuggestions;
+ this.mClientState = clientState;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * The {@link FillWindow} used to display the Autofill UI.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable FillWindow getFillWindow() {
+ return mFillWindow;
+ }
+
+ /**
+ * The {@link Dataset}s representing the inline suggestions data. Defaults to null if no
+ * inline suggestions are available from the service.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable List<Dataset> getInlineSuggestions() {
+ return mInlineSuggestions;
+ }
+
+ /**
+ * The client state that {@link AugmentedAutofillService} implementation can put anything in to
+ * identify the request and the response when calling
+ * {@link AugmentedAutofillService#getFillEventHistory()}.
+ *
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public @Nullable Bundle getClientState() {
+ return mClientState;
+ }
+
+ /**
+ * A builder for {@link FillResponse}
+ */
+ @SuppressWarnings("WeakerAccess")
+ @DataClass.Generated.Member
+ public static final class Builder extends BaseBuilder {
+
+ private @Nullable FillWindow mFillWindow;
+ private @Nullable List<Dataset> mInlineSuggestions;
+ private @Nullable Bundle mClientState;
+
+ private long mBuilderFieldsSet = 0L;
+
+ public Builder() {
+ }
+
+ /**
+ * The {@link FillWindow} used to display the Autofill UI.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setFillWindow(@NonNull FillWindow value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x1;
+ mFillWindow = value;
+ return this;
+ }
+
+ /**
+ * The {@link Dataset}s representing the inline suggestions data. Defaults to null if no
+ * inline suggestions are available from the service.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setInlineSuggestions(@NonNull List<Dataset> value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x2;
+ mInlineSuggestions = value;
+ return this;
+ }
+
+ /** @see #setInlineSuggestions */
+ @DataClass.Generated.Member
+ @Override
+ @NonNull FillResponse.Builder addInlineSuggestion(@NonNull Dataset value) {
+ if (mInlineSuggestions == null) setInlineSuggestions(new ArrayList<>());
+ mInlineSuggestions.add(value);
+ return this;
+ }
+
+ /**
+ * The client state that {@link AugmentedAutofillService} implementation can put anything in to
+ * identify the request and the response when calling
+ * {@link AugmentedAutofillService#getFillEventHistory()}.
+ */
+ @DataClass.Generated.Member
+ public @NonNull Builder setClientState(@NonNull Bundle value) {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x4;
+ mClientState = value;
+ return this;
+ }
+
+ /** Builds the instance. This builder should not be touched after calling this! */
+ public @NonNull FillResponse build() {
+ checkNotUsed();
+ mBuilderFieldsSet |= 0x8; // Mark builder used
+
+ if ((mBuilderFieldsSet & 0x1) == 0) {
+ mFillWindow = defaultFillWindow();
+ }
+ if ((mBuilderFieldsSet & 0x2) == 0) {
+ mInlineSuggestions = defaultInlineSuggestions();
+ }
+ if ((mBuilderFieldsSet & 0x4) == 0) {
+ mClientState = defaultClientState();
+ }
+ FillResponse o = new FillResponse(
+ mFillWindow,
+ mInlineSuggestions,
+ mClientState);
+ return o;
+ }
+
+ private void checkNotUsed() {
+ if ((mBuilderFieldsSet & 0x8) != 0) {
+ throw new IllegalStateException(
+ "This Builder should not be reused. Use a new Builder instance instead");
+ }
+ }
+ }
+
+ @DataClass.Generated(
+ time = 1584480900526L,
+ codegenVersion = "1.0.15",
+ sourceFile = "frameworks/base/core/java/android/service/autofill/augmented/FillResponse.java",
+ inputSignatures = "private @android.annotation.Nullable android.service.autofill.augmented.FillWindow mFillWindow\nprivate @com.android.internal.util.DataClass.PluralOf(\"inlineSuggestion\") @android.annotation.Nullable java.util.List<android.service.autofill.Dataset> mInlineSuggestions\nprivate @android.annotation.Nullable android.os.Bundle mClientState\nprivate static android.service.autofill.augmented.FillWindow defaultFillWindow()\nprivate static java.util.List<android.service.autofill.Dataset> defaultInlineSuggestions()\nprivate static android.os.Bundle defaultClientState()\nclass FillResponse extends java.lang.Object implements []\[email protected](genBuilder=true, genHiddenGetters=true)\nabstract android.service.autofill.augmented.FillResponse.Builder addInlineSuggestion(android.service.autofill.Dataset)\nclass BaseBuilder extends java.lang.Object implements []")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/android/service/autofill/augmented/FillWindow.java b/android/service/autofill/augmented/FillWindow.java
new file mode 100644
index 0000000..077df6c
--- /dev/null
+++ b/android/service/autofill/augmented/FillWindow.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2018 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.autofill.augmented;
+
+import static android.service.autofill.augmented.AugmentedAutofillService.sDebug;
+import static android.service.autofill.augmented.AugmentedAutofillService.sVerbose;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
+import android.service.autofill.augmented.PresentationParams.Area;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.autofill.IAutofillWindowPresenter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import dalvik.system.CloseGuard;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+
+/**
+ * Handle to a window used to display the augmented autofill UI.
+ *
+ * <p>The steps to create an augmented autofill UI are:
+ *
+ * <ol>
+ * <li>Gets the {@link PresentationParams} from the {@link FillRequest}.
+ * <li>Gets the {@link Area} to display the UI (for example, through
+ * {@link PresentationParams#getSuggestionArea()}.
+ * <li>Creates a {@link View} that must fit in the {@link Area#getBounds() area boundaries}.
+ * <li>Set the proper listeners to the view (for example, a click listener that
+ * triggers {@link FillController#autofill(java.util.List)}
+ * <li>Call {@link #update(Area, View, long)} with these arguments.
+ * <li>Create a {@link FillResponse} with the {@link FillWindow}.
+ * <li>Pass such {@link FillResponse} to {@link FillCallback#onSuccess(FillResponse)}.
+ * </ol>
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class FillWindow implements AutoCloseable {
+ private static final String TAG = FillWindow.class.getSimpleName();
+
+ private final Object mLock = new Object();
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private final @NonNull Handler mUiThreadHandler = new Handler(Looper.getMainLooper());
+
+ @GuardedBy("mLock")
+ private @NonNull WindowManager mWm;
+ @GuardedBy("mLock")
+ private View mFillView;
+ @GuardedBy("mLock")
+ private boolean mShowing;
+ @GuardedBy("mLock")
+ private @Nullable Rect mBounds;
+
+ @GuardedBy("mLock")
+ private boolean mUpdateCalled;
+ @GuardedBy("mLock")
+ private boolean mDestroyed;
+
+ private @NonNull AutofillProxy mProxy;
+
+ /**
+ * Updates the content of the window.
+ *
+ * @param rootView new root view
+ * @param area coordinates to render the view.
+ * @param flags currently not used.
+ *
+ * @return boolean whether the window was updated or not.
+ *
+ * @throws IllegalArgumentException if the area is not compatible with this window
+ */
+ public boolean update(@NonNull Area area, @NonNull View rootView, long flags) {
+ if (sDebug) {
+ Log.d(TAG, "Updating " + area + " + with " + rootView);
+ }
+ // TODO(b/123100712): add test case for null
+ Preconditions.checkNotNull(area);
+ Preconditions.checkNotNull(area.proxy);
+ Preconditions.checkNotNull(rootView);
+ // TODO(b/123100712): must check the area is a valid object returned by
+ // SmartSuggestionParams, throw IAE if not
+
+ final PresentationParams smartSuggestion = area.proxy.getSmartSuggestionParams();
+ if (smartSuggestion == null) {
+ Log.w(TAG, "No SmartSuggestionParams");
+ return false;
+ }
+
+ final Rect rect = area.getBounds();
+ if (rect == null) {
+ Log.wtf(TAG, "No Rect on SmartSuggestionParams");
+ return false;
+ }
+
+ synchronized (mLock) {
+ checkNotDestroyedLocked();
+
+ mProxy = area.proxy;
+
+ // TODO(b/123227534): once we have the SurfaceControl approach, we should update the
+ // window instead of destroying. In fact, it might be better to allocate a full window
+ // initially, which is transparent (and let touches get through) everywhere but in the
+ // rect boundaries.
+
+ // TODO(b/123099468): make sure all touch events are handled, window is always closed,
+ // etc.
+
+ mWm = rootView.getContext().getSystemService(WindowManager.class);
+ mFillView = rootView;
+ // Listen to the touch outside to destroy the window when typing is detected.
+ mFillView.setOnTouchListener(
+ (view, motionEvent) -> {
+ if (motionEvent.getAction() == MotionEvent.ACTION_OUTSIDE) {
+ if (sVerbose) Log.v(TAG, "Outside touch detected, hiding the window");
+ hide();
+ }
+ return false;
+ }
+ );
+ mShowing = false;
+ mBounds = new Rect(area.getBounds());
+ if (sDebug) {
+ Log.d(TAG, "Created FillWindow: params= " + smartSuggestion + " view=" + rootView);
+ }
+ mUpdateCalled = true;
+ mDestroyed = false;
+ mProxy.setFillWindow(this);
+ return true;
+ }
+ }
+
+ /** @hide */
+ void show() {
+ // TODO(b/123100712): check if updated first / throw exception
+ if (sDebug) Log.d(TAG, "show()");
+ synchronized (mLock) {
+ checkNotDestroyedLocked();
+ if (mWm == null || mFillView == null) {
+ throw new IllegalStateException("update() not called yet, or already destroyed()");
+ }
+ if (mProxy != null) {
+ try {
+ mProxy.requestShowFillUi(mBounds.right - mBounds.left,
+ mBounds.bottom - mBounds.top,
+ /*anchorBounds=*/ null, new FillWindowPresenter(this));
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error requesting to show fill window", e);
+ }
+ mProxy.logEvent(AutofillProxy.REPORT_EVENT_UI_SHOWN);
+ }
+ }
+ }
+
+ /**
+ * Hides the window.
+ *
+ * <p>The window is not destroyed and can be shown again
+ */
+ private void hide() {
+ if (sDebug) Log.d(TAG, "hide()");
+ synchronized (mLock) {
+ checkNotDestroyedLocked();
+ if (mWm == null || mFillView == null) {
+ throw new IllegalStateException("update() not called yet, or already destroyed()");
+ }
+ if (mProxy != null && mShowing) {
+ try {
+ mProxy.requestHideFillUi();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error requesting to hide fill window", e);
+ }
+ }
+ }
+ }
+
+ private void handleShow(WindowManager.LayoutParams p) {
+ if (sDebug) Log.d(TAG, "handleShow()");
+ synchronized (mLock) {
+ if (mWm != null && mFillView != null) {
+ p.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+ if (!mShowing) {
+ mWm.addView(mFillView, p);
+ mShowing = true;
+ } else {
+ mWm.updateViewLayout(mFillView, p);
+ }
+ }
+ }
+ }
+
+ private void handleHide() {
+ if (sDebug) Log.d(TAG, "handleHide()");
+ synchronized (mLock) {
+ if (mWm != null && mFillView != null && mShowing) {
+ mWm.removeView(mFillView);
+ mShowing = false;
+ }
+ }
+ }
+
+ /**
+ * Destroys the window.
+ *
+ * <p>Once destroyed, this window cannot be used anymore
+ */
+ public void destroy() {
+ if (sDebug) {
+ Log.d(TAG,
+ "destroy(): mDestroyed=" + mDestroyed + " mShowing=" + mShowing + " mFillView="
+ + mFillView);
+ }
+ synchronized (mLock) {
+ if (mDestroyed) return;
+ if (mUpdateCalled) {
+ mFillView.setOnClickListener(null);
+ hide();
+ mProxy.logEvent(AutofillProxy.REPORT_EVENT_UI_DESTROYED);
+ }
+ mDestroyed = true;
+ mCloseGuard.close();
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ mCloseGuard.warnIfOpen();
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void checkNotDestroyedLocked() {
+ if (mDestroyed) {
+ throw new IllegalStateException("already destroyed()");
+ }
+ }
+
+ /** @hide */
+ public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ synchronized (this) {
+ pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
+ pw.print(prefix); pw.print("updateCalled: "); pw.println(mUpdateCalled);
+ if (mFillView != null) {
+ pw.print(prefix); pw.print("fill window: ");
+ pw.println(mShowing ? "shown" : "hidden");
+ pw.print(prefix); pw.print("fill view: ");
+ pw.println(mFillView);
+ pw.print(prefix); pw.print("mBounds: ");
+ pw.println(mBounds);
+ pw.print(prefix); pw.print("mWm: ");
+ pw.println(mWm);
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void close() {
+ destroy();
+ }
+
+ private static final class FillWindowPresenter extends IAutofillWindowPresenter.Stub {
+ private final @NonNull WeakReference<FillWindow> mFillWindowReference;
+
+ FillWindowPresenter(@NonNull FillWindow fillWindow) {
+ mFillWindowReference = new WeakReference<>(fillWindow);
+ }
+
+ @Override
+ public void show(WindowManager.LayoutParams p, Rect transitionEpicenter,
+ boolean fitsSystemWindows, int layoutDirection) {
+ if (sDebug) Log.d(TAG, "FillWindowPresenter.show()");
+ final FillWindow fillWindow = mFillWindowReference.get();
+ if (fillWindow != null) {
+ fillWindow.mUiThreadHandler.sendMessage(
+ obtainMessage(FillWindow::handleShow, fillWindow, p));
+ }
+ }
+
+ @Override
+ public void hide(Rect transitionEpicenter) {
+ if (sDebug) Log.d(TAG, "FillWindowPresenter.hide()");
+ final FillWindow fillWindow = mFillWindowReference.get();
+ if (fillWindow != null) {
+ fillWindow.mUiThreadHandler.sendMessage(
+ obtainMessage(FillWindow::handleHide, fillWindow));
+ }
+ }
+ }
+}
diff --git a/android/service/autofill/augmented/Helper.java b/android/service/autofill/augmented/Helper.java
new file mode 100644
index 0000000..afcd8b7
--- /dev/null
+++ b/android/service/autofill/augmented/Helper.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 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.autofill.augmented;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.metrics.LogMaker;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+/** @hide */
+public final class Helper {
+
+ private static final MetricsLogger sMetricsLogger = new MetricsLogger();
+
+ /**
+ * Logs a {@code MetricsEvent.AUTOFILL_AUGMENTED_RESPONSE} event.
+ */
+ public static void logResponse(int type, @NonNull String servicePackageName,
+ @NonNull ComponentName componentName, int mSessionId, long durationMs) {
+ final LogMaker log = new LogMaker(MetricsEvent.AUTOFILL_AUGMENTED_RESPONSE)
+ .setType(type)
+ .setComponentName(componentName)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SESSION_ID, mSessionId)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_DURATION, durationMs);
+ sMetricsLogger.write(log);
+ }
+
+ private Helper() {
+ throw new UnsupportedOperationException("contains only static methods");
+ }
+}
diff --git a/android/service/autofill/augmented/PresentationParams.java b/android/service/autofill/augmented/PresentationParams.java
new file mode 100644
index 0000000..8b3a001
--- /dev/null
+++ b/android/service/autofill/augmented/PresentationParams.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2018 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.autofill.augmented;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.graphics.Rect;
+import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy;
+import android.view.View;
+
+import java.io.PrintWriter;
+
+/**
+ * Abstraction of a "Smart Suggestion" component responsible to embed the autofill UI provided by
+ * the augmented autofill service.
+ *
+ * <p>The Smart Suggestion is represented by a {@link Area} object that contains the
+ * dimensions the smart suggestion window, so the service can use it to calculate the size of the
+ * view that will be passed to {@link FillWindow#update(Area, View, long)}.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public abstract class PresentationParams {
+
+ // /** @hide */
+ PresentationParams() {}
+
+ /**
+ * Gets the area of the suggestion strip for the given {@code metadata}
+ *
+ * @return strip dimensions, or {@code null} if the Smart Suggestion provider does not support
+ * suggestions strip.
+ */
+ @Nullable
+ public Area getSuggestionArea() {
+ return null;
+ }
+
+ abstract void dump(String prefix, PrintWriter pw);
+
+ /**
+ * Area associated with a {@link PresentationParams Smart Suggestions} provider.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public abstract static class Area {
+
+ /** @hide */
+ public final AutofillProxy proxy;
+
+ private final Rect mBounds;
+
+ private Area(@NonNull AutofillProxy proxy, @NonNull Rect bounds) {
+ this.proxy = proxy;
+ mBounds = bounds;
+ }
+
+ /**
+ * Gets the area boundaries.
+ */
+ @NonNull
+ public Rect getBounds() {
+ return mBounds;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return mBounds.toString();
+ }
+ }
+
+ /**
+ * System-provided poup window anchored to a view.
+ *
+ * <p>Used just for debugging purposes.
+ *
+ * @hide
+ */
+ public static final class SystemPopupPresentationParams extends PresentationParams {
+ private final Area mSuggestionArea;
+
+ public SystemPopupPresentationParams(@NonNull AutofillProxy proxy, @NonNull Rect rect) {
+ mSuggestionArea = new Area(proxy, rect) {};
+ }
+
+ @Override
+ public Area getSuggestionArea() {
+ return mSuggestionArea;
+ }
+
+ @Override
+ void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ pw.print(prefix); pw.print("area: "); pw.println(mSuggestionArea);
+ }
+ }
+}
diff --git a/android/service/carrier/ApnService.java b/android/service/carrier/ApnService.java
new file mode 100644
index 0000000..0c12fd4
--- /dev/null
+++ b/android/service/carrier/ApnService.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 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.carrier;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.WorkerThread;
+import android.app.Service;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+import android.service.carrier.IApnSourceService;
+
+import java.util.List;
+
+/**
+ * A service that the system can call to restore default APNs.
+ * <p>
+ * To extend this class, specify the full name of your implementation in the resource file
+ * {@code packages/providers/TelephonyProvider/res/values/config.xml} as the
+ * {@code apn_source_service}.
+ * </p>
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class ApnService extends Service {
+
+ private static final String LOG_TAG = "ApnService";
+
+ private final IApnSourceService.Stub mBinder = new IApnSourceService.Stub() {
+ /**
+ * Retreive APNs for the default slot index.
+ */
+ @Override
+ public ContentValues[] getApns(int subId) {
+ try {
+ List<ContentValues> apns = ApnService.this.onRestoreApns(subId);
+ return apns.toArray(new ContentValues[apns.size()]);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Error in getApns for subId=" + subId + ": " + e.getMessage(), e);
+ return null;
+ }
+ }
+ };
+
+ @Override
+ @NonNull
+ public IBinder onBind(@Nullable Intent intent) {
+ return mBinder;
+ }
+
+ /**
+ * Override this method to restore default user APNs with a carrier service instead of the
+ * built in platform xml APNs list.
+ * <p>
+ * This method is called by the TelephonyProvider when the user requests restoring the default
+ * APNs. It should return a list of ContentValues representing the default APNs for the given
+ * subId.
+ */
+ @WorkerThread
+ @NonNull
+ public abstract List<ContentValues> onRestoreApns(int subId);
+}
diff --git a/android/service/carrier/CarrierIdentifier.java b/android/service/carrier/CarrierIdentifier.java
new file mode 100644
index 0000000..bc0f909
--- /dev/null
+++ b/android/service/carrier/CarrierIdentifier.java
@@ -0,0 +1,255 @@
+/**
+ * Copyright (c) 2015, 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.carrier;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.telephony.Rlog;
+
+import java.util.Objects;
+
+/**
+ * Used to pass info to CarrierConfigService implementations so they can decide what values to
+ * return. Instead of passing mcc, mnc, gid1, gid2, spn, imsi to locate carrier information,
+ * CarrierIdentifier also include carrier id {@link TelephonyManager#getSimCarrierId()},
+ * a platform-wide unique identifier for each carrier. CarrierConfigService can directly use
+ * carrier id as the key to look up the carrier info.
+ */
+public class CarrierIdentifier implements Parcelable {
+
+ /** Used to create a {@link CarrierIdentifier} from a {@link Parcel}. */
+ public static final @android.annotation.NonNull Creator<CarrierIdentifier> CREATOR = new Creator<CarrierIdentifier>() {
+ @Override
+ public CarrierIdentifier createFromParcel(Parcel parcel) {
+ return new CarrierIdentifier(parcel);
+ }
+
+ @Override
+ public CarrierIdentifier[] newArray(int i) {
+ return new CarrierIdentifier[i];
+ }
+ };
+
+ private String mMcc;
+ private String mMnc;
+ private @Nullable String mSpn;
+ private @Nullable String mImsi;
+ private @Nullable String mGid1;
+ private @Nullable String mGid2;
+ private int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
+ private int mSpecificCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
+
+ public CarrierIdentifier(String mcc, String mnc, @Nullable String spn, @Nullable String imsi,
+ @Nullable String gid1, @Nullable String gid2) {
+ this(mcc, mnc, spn, imsi, gid1, gid2, TelephonyManager.UNKNOWN_CARRIER_ID,
+ TelephonyManager.UNKNOWN_CARRIER_ID);
+ }
+
+ /**
+ * @param mcc mobile country code
+ * @param mnc mobile network code
+ * @param spn service provider name
+ * @param imsi International Mobile Subscriber Identity {@link TelephonyManager#getSubscriberId()}
+ * @param gid1 group id level 1 {@link TelephonyManager#getGroupIdLevel1()}
+ * @param gid2 group id level 2
+ * @param carrierid carrier unique identifier {@link TelephonyManager#getSimCarrierId()}, used
+ * to uniquely identify the carrier and look up the carrier configurations.
+ * @param specificCarrierId specific carrier identifier
+ * {@link TelephonyManager#getSimSpecificCarrierId()}
+ */
+ public CarrierIdentifier(@NonNull String mcc, @NonNull String mnc, @Nullable String spn,
+ @Nullable String imsi, @Nullable String gid1, @Nullable String gid2,
+ int carrierid, int specificCarrierId) {
+ mMcc = mcc;
+ mMnc = mnc;
+ mSpn = spn;
+ mImsi = imsi;
+ mGid1 = gid1;
+ mGid2 = gid2;
+ mCarrierId = carrierid;
+ mSpecificCarrierId = specificCarrierId;
+ }
+
+ /**
+ * Creates a carrier identifier instance.
+ *
+ * @param mccMnc A 3-byte array as defined by 3GPP TS 24.008.
+ * @param gid1 The group identifier level 1.
+ * @param gid2 The group identifier level 2.
+ * @throws IllegalArgumentException If the length of {@code mccMnc} is not 3.
+ */
+ public CarrierIdentifier(byte[] mccMnc, @Nullable String gid1, @Nullable String gid2) {
+ if (mccMnc.length != 3) {
+ throw new IllegalArgumentException(
+ "MCC & MNC must be set by a 3-byte array: byte[" + mccMnc.length + "]");
+ }
+ String hex = IccUtils.bytesToHexString(mccMnc);
+ mMcc = new String(new char[] {hex.charAt(1), hex.charAt(0), hex.charAt(3)});
+ if (hex.charAt(2) == 'F') {
+ mMnc = new String(new char[] {hex.charAt(5), hex.charAt(4)});
+ } else {
+ mMnc = new String(new char[] {hex.charAt(5), hex.charAt(4), hex.charAt(2)});
+ }
+ mGid1 = gid1;
+ mGid2 = gid2;
+ mSpn = null;
+ mImsi = null;
+ }
+
+ /** @hide */
+ public CarrierIdentifier(Parcel parcel) {
+ readFromParcel(parcel);
+ }
+
+ /** Get the mobile country code. */
+ public String getMcc() {
+ return mMcc;
+ }
+
+ /** Get the mobile network code. */
+ public String getMnc() {
+ return mMnc;
+ }
+
+ /** Get the service provider name. */
+ @Nullable
+ public String getSpn() {
+ return mSpn;
+ }
+
+ /** Get the international mobile subscriber identity. */
+ @Nullable
+ public String getImsi() {
+ return mImsi;
+ }
+
+ /** Get the group identifier level 1. */
+ @Nullable
+ public String getGid1() {
+ return mGid1;
+ }
+
+ /** Get the group identifier level 2. */
+ @Nullable
+ public String getGid2() {
+ return mGid2;
+ }
+
+ /**
+ * Returns the carrier id.
+ * @see TelephonyManager#getSimCarrierId()
+ */
+ public int getCarrierId() {
+ return mCarrierId;
+ }
+
+ /**
+ * A specific carrier ID returns the fine-grained carrier ID of the current subscription.
+ * It can represent the fact that a carrier may be in effect an aggregation of other carriers
+ * (ie in an MVNO type scenario) where each of these specific carriers which are used to make
+ * up the actual carrier service may have different carrier configurations.
+ * A specific carrier ID could also be used, for example, in a scenario where a carrier requires
+ * different carrier configuration for different service offering such as a prepaid plan.
+ *
+ * @see TelephonyManager#getSimSpecificCarrierId()
+ */
+ public int getSpecificCarrierId() {
+ return mSpecificCarrierId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ CarrierIdentifier that = (CarrierIdentifier) obj;
+ return Objects.equals(mMcc, that.mMcc)
+ && Objects.equals(mMnc, that.mMnc)
+ && Objects.equals(mSpn, that.mSpn)
+ && Objects.equals(mImsi, that.mImsi)
+ && Objects.equals(mGid1, that.mGid1)
+ && Objects.equals(mGid2, that.mGid2)
+ && Objects.equals(mCarrierId, that.mCarrierId)
+ && Objects.equals(mSpecificCarrierId, that.mSpecificCarrierId);
+ }
+
+ @Override
+ public int hashCode(){
+ return Objects.hash(mMcc, mMnc, mSpn, mImsi, mGid1, mGid2, mCarrierId, mSpecificCarrierId);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mMcc);
+ out.writeString(mMnc);
+ out.writeString(mSpn);
+ out.writeString(mImsi);
+ out.writeString(mGid1);
+ out.writeString(mGid2);
+ out.writeInt(mCarrierId);
+ out.writeInt(mSpecificCarrierId);
+ }
+
+ @Override
+ public String toString() {
+ return "CarrierIdentifier{"
+ + "mcc=" + mMcc
+ + ",mnc=" + mMnc
+ + ",spn=" + mSpn
+ + ",imsi=" + Rlog.pii(false, mImsi)
+ + ",gid1=" + mGid1
+ + ",gid2=" + mGid2
+ + ",carrierid=" + mCarrierId
+ + ",specificCarrierId=" + mSpecificCarrierId
+ + "}";
+ }
+
+ /** @hide */
+ public void readFromParcel(Parcel in) {
+ mMcc = in.readString();
+ mMnc = in.readString();
+ mSpn = in.readString();
+ mImsi = in.readString();
+ mGid1 = in.readString();
+ mGid2 = in.readString();
+ mCarrierId = in.readInt();
+ mSpecificCarrierId = in.readInt();
+ }
+
+ /** @hide */
+ public interface MatchType {
+ int ALL = 0;
+ int SPN = 1;
+ int IMSI_PREFIX = 2;
+ int GID1 = 3;
+ int GID2 = 4;
+ }
+}
diff --git a/android/service/carrier/CarrierMessagingClientService.java b/android/service/carrier/CarrierMessagingClientService.java
new file mode 100644
index 0000000..767c1d1
--- /dev/null
+++ b/android/service/carrier/CarrierMessagingClientService.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 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.carrier;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * If the default SMS app has a service that extends this class, the system always tries to bind
+ * it so that the process is always running, which allows the app to have a persistent connection
+ * to the server.
+ *
+ * <p>The service must have an
+ * {@link android.telephony.TelephonyManager#ACTION_CARRIER_MESSAGING_CLIENT_SERVICE}
+ * action in the intent handler, and be protected with
+ * {@link android.Manifest.permission#BIND_CARRIER_MESSAGING_CLIENT_SERVICE}.
+ * However the service does not have to be exported.
+ *
+ * <p>The service must be associated with a non-main process, meaning it must have an
+ * {@code android:process} tag in its manifest entry.
+ *
+ * <p>An app can use
+ * {@link android.content.pm.PackageManager#setComponentEnabledSetting(ComponentName, int, int)}
+ * to disable or enable the service. An app should use it to disable the service when it no longer
+ * needs to be running.
+ *
+ * <p>When the owner process crashes, the service will be re-bound automatically after a
+ * back-off.
+ *
+ * <p>Note the process may still be killed if the system is under heavy memory pressure, in which
+ * case the process will be re-started later.
+ *
+ * <p>Example: First, define a subclass in the application:
+ * <pre>
+ * public class MyCarrierMessagingClientService extends CarrierMessagingClientService {
+ * }
+ * </pre>
+ * Then, declare it in its {@code AndroidManifest.xml}:
+ * <pre>
+ * <service
+ * android:name=".MyCarrierMessagingClientService"
+ * android:exported="false"
+ * android:process=":persistent"
+ * android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE" />
+ * </intent-filter>
+ * </service>
+ * </pre>
+ */
+public class CarrierMessagingClientService extends Service {
+ private final ICarrierMessagingClientServiceImpl mImpl;
+
+ public CarrierMessagingClientService() {
+ mImpl = new ICarrierMessagingClientServiceImpl();
+ }
+
+ @Override
+ @NonNull
+ public final IBinder onBind(@Nullable Intent intent) {
+ return mImpl.asBinder();
+ }
+
+ private class ICarrierMessagingClientServiceImpl extends ICarrierMessagingClientService.Stub {
+ }
+}
diff --git a/android/service/carrier/CarrierMessagingService.java b/android/service/carrier/CarrierMessagingService.java
new file mode 100644
index 0000000..88a78c3
--- /dev/null
+++ b/android/service/carrier/CarrierMessagingService.java
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2014 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.carrier;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.app.Service;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.util.List;
+
+/**
+ * A service that receives calls from the system when new SMS and MMS are
+ * sent or received.
+ * <p>To extend this class, you must declare the service in your manifest file with
+ * the {@link android.Manifest.permission#BIND_CARRIER_SERVICES} permission
+ * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
+ * <pre>
+ * <service android:name=".MyMessagingService"
+ * android:label="@string/service_name"
+ * android:permission="android.permission.BIND_CARRIER_SERVICES">
+ * <intent-filter>
+ * <action android:name="android.service.carrier.CarrierMessagingService" />
+ * </intent-filter>
+ * </service></pre>
+ */
+public abstract class CarrierMessagingService extends Service {
+ /**
+ * The {@link android.content.Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE
+ = "android.service.carrier.CarrierMessagingService";
+
+ /**
+ * The default bitmask value passed to the callback of {@link #onReceiveTextSms} with all
+ * {@code RECEIVE_OPTIONS_x} flags cleared to indicate that the message should be kept and a
+ * new message notification should be shown.
+ *
+ * @see #RECEIVE_OPTIONS_DROP
+ * @see #RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE
+ */
+ public static final int RECEIVE_OPTIONS_DEFAULT = 0;
+
+ /**
+ * Used to set the flag in the bitmask passed to the callback of {@link #onReceiveTextSms} to
+ * indicate that the inbound SMS should be dropped.
+ */
+ public static final int RECEIVE_OPTIONS_DROP = 0x1;
+
+ /**
+ * Used to set the flag in the bitmask passed to the callback of {@link #onReceiveTextSms} to
+ * indicate that a new message notification should not be shown to the user when the
+ * credential-encrypted storage of the device is not available before the user unlocks the
+ * phone. It is only applicable to devices that support file-based encryption.
+ */
+ public static final int RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE = 0x2;
+
+ /**
+ * Indicates that an SMS or MMS message was successfully sent.
+ */
+ public static final int SEND_STATUS_OK = 0;
+
+ /**
+ * SMS/MMS sending failed. We should retry via the carrier network.
+ */
+ public static final int SEND_STATUS_RETRY_ON_CARRIER_NETWORK = 1;
+
+ /**
+ * SMS/MMS sending failed. We should not retry via the carrier network.
+ */
+ public static final int SEND_STATUS_ERROR = 2;
+
+ /**
+ * Successfully downloaded an MMS message.
+ */
+ public static final int DOWNLOAD_STATUS_OK = 0;
+
+ /**
+ * MMS downloading failed. We should retry via the carrier network.
+ */
+ public static final int DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK = 1;
+
+ /**
+ * MMS downloading failed. We should not retry via the carrier network.
+ */
+ public static final int DOWNLOAD_STATUS_ERROR = 2;
+
+ /**
+ * Flag to request SMS delivery status report.
+ */
+ public static final int SEND_FLAG_REQUEST_DELIVERY_STATUS = 1;
+
+ private final ICarrierMessagingWrapper mWrapper = new ICarrierMessagingWrapper();
+
+ /**
+ * Override this method to filter inbound SMS messages.
+ *
+ * @param pdu the PDUs of the message
+ * @param format the format of the PDUs, typically "3gpp" or "3gpp2"
+ * @param destPort the destination port of a binary SMS, this will be -1 for text SMS
+ * @param subId SMS subscription ID of the SIM
+ * @param callback result callback. Call with {@code true} to keep an inbound SMS message and
+ * deliver to SMS apps, and {@code false} to drop the message.
+ * @deprecated Use {@link #onReceiveTextSms} instead.
+ */
+ @Deprecated
+ public void onFilterSms(@NonNull MessagePdu pdu, @NonNull String format, int destPort,
+ int subId, @NonNull ResultCallback<Boolean> callback) {
+ // optional
+ try {
+ callback.onReceiveResult(true);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Override this method to filter inbound SMS messages.
+ *
+ * <p>This method will be called once for every incoming text SMS. You can invoke the callback
+ * with a bitmask to tell the platform how to handle the SMS. For a SMS received on a
+ * file-based encryption capable device while the credential-encrypted storage is not available,
+ * this method will be called for the second time when the credential-encrypted storage becomes
+ * available after the user unlocks the phone, if the bit {@link #RECEIVE_OPTIONS_DROP} is not
+ * set when invoking the callback.
+ *
+ * @param pdu the PDUs of the message
+ * @param format the format of the PDUs, typically "3gpp" or "3gpp2"
+ * @param destPort the destination port of a binary SMS, this will be -1 for text SMS
+ * @param subId SMS subscription ID of the SIM
+ * @param callback result callback. Call with a bitmask integer to indicate how the incoming
+ * text SMS should be handled by the platform. Use {@link #RECEIVE_OPTIONS_DROP} and
+ * {@link #RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE}
+ * to set the flags in the bitmask.
+ */
+ public void onReceiveTextSms(@NonNull MessagePdu pdu, @NonNull String format,
+ int destPort, int subId, @NonNull final ResultCallback<Integer> callback) {
+ onFilterSms(pdu, format, destPort, subId, new ResultCallback<Boolean>() {
+ @Override
+ public void onReceiveResult(Boolean result) throws RemoteException {
+ callback.onReceiveResult(result ? RECEIVE_OPTIONS_DEFAULT : RECEIVE_OPTIONS_DROP
+ | RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE);
+ }
+ });
+ }
+
+ /**
+ * Override this method to intercept text SMSs sent from the device.
+ * @deprecated Override {@link #onSendTextSms} below instead.
+ *
+ * @param text the text to send
+ * @param subId SMS subscription ID of the SIM
+ * @param destAddress phone number of the recipient of the message
+ * @param callback result callback. Call with a {@link SendSmsResult}.
+ */
+ @Deprecated
+ public void onSendTextSms(
+ @NonNull String text, int subId, @NonNull String destAddress,
+ @NonNull ResultCallback<SendSmsResult> callback) {
+ // optional
+ try {
+ callback.onReceiveResult(new SendSmsResult(SEND_STATUS_RETRY_ON_CARRIER_NETWORK, 0));
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Override this method to intercept text SMSs sent from the device.
+ *
+ * @param text the text to send
+ * @param subId SMS subscription ID of the SIM
+ * @param destAddress phone number of the recipient of the message
+ * @param sendSmsFlag Flag for sending SMS. Acceptable values are 0 and
+ * {@link #SEND_FLAG_REQUEST_DELIVERY_STATUS}.
+ * @param callback result callback. Call with a {@link SendSmsResult}.
+ */
+ public void onSendTextSms(
+ @NonNull String text, int subId, @NonNull String destAddress,
+ int sendSmsFlag, @NonNull ResultCallback<SendSmsResult> callback) {
+ // optional
+ onSendTextSms(text, subId, destAddress, callback);
+ }
+
+ /**
+ * Override this method to intercept binary SMSs sent from the device.
+ * @deprecated Override {@link #onSendDataSms} below instead.
+ *
+ * @param data the binary content
+ * @param subId SMS subscription ID of the SIM
+ * @param destAddress phone number of the recipient of the message
+ * @param destPort the destination port
+ * @param callback result callback. Call with a {@link SendSmsResult}.
+ */
+ @Deprecated
+ public void onSendDataSms(@NonNull byte[] data, int subId,
+ @NonNull String destAddress, int destPort,
+ @NonNull ResultCallback<SendSmsResult> callback) {
+ // optional
+ try {
+ callback.onReceiveResult(new SendSmsResult(SEND_STATUS_RETRY_ON_CARRIER_NETWORK, 0));
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Override this method to intercept binary SMSs sent from the device.
+ *
+ * @param data the binary content
+ * @param subId SMS subscription ID of the SIM
+ * @param destAddress phone number of the recipient of the message
+ * @param destPort the destination port
+ * @param sendSmsFlag Flag for sending SMS. Acceptable values are 0 and
+ * {@link #SEND_FLAG_REQUEST_DELIVERY_STATUS}.
+ * @param callback result callback. Call with a {@link SendSmsResult}.
+ */
+ public void onSendDataSms(@NonNull byte[] data, int subId,
+ @NonNull String destAddress, int destPort, int sendSmsFlag,
+ @NonNull ResultCallback<SendSmsResult> callback) {
+ // optional
+ onSendDataSms(data, subId, destAddress, destPort, callback);
+ }
+
+ /**
+ * Override this method to intercept long SMSs sent from the device.
+ * @deprecated Override {@link #onSendMultipartTextSms} below instead.
+ *
+ * @param parts a {@link List} of the message parts
+ * @param subId SMS subscription ID of the SIM
+ * @param destAddress phone number of the recipient of the message
+ * @param callback result callback. Call with a {@link SendMultipartSmsResult}.
+ */
+ @Deprecated
+ public void onSendMultipartTextSms(@NonNull List<String> parts,
+ int subId, @NonNull String destAddress,
+ @NonNull ResultCallback<SendMultipartSmsResult> callback) {
+ // optional
+ try {
+ callback.onReceiveResult(
+ new SendMultipartSmsResult(SEND_STATUS_RETRY_ON_CARRIER_NETWORK, null));
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Override this method to intercept long SMSs sent from the device.
+ *
+ * @param parts a {@link List} of the message parts
+ * @param subId SMS subscription ID of the SIM
+ * @param destAddress phone number of the recipient of the message
+ * @param sendSmsFlag Flag for sending SMS. Acceptable values are 0 and
+ * {@link #SEND_FLAG_REQUEST_DELIVERY_STATUS}.
+ * @param callback result callback. Call with a {@link SendMultipartSmsResult}.
+ */
+ public void onSendMultipartTextSms(@NonNull List<String> parts,
+ int subId, @NonNull String destAddress, int sendSmsFlag,
+ @NonNull ResultCallback<SendMultipartSmsResult> callback) {
+ // optional
+ onSendMultipartTextSms(parts, subId, destAddress, callback);
+ }
+
+ /**
+ * Override this method to intercept MMSs sent from the device.
+ *
+ * @param pduUri the content provider URI of the PDU to send
+ * @param subId SMS subscription ID of the SIM
+ * @param location the optional URI to send this MMS PDU. If this is {code null},
+ * the PDU should be sent to the default MMSC URL.
+ * @param callback result callback. Call with a {@link SendMmsResult}.
+ */
+ public void onSendMms(@NonNull Uri pduUri, int subId,
+ @Nullable Uri location, @NonNull ResultCallback<SendMmsResult> callback) {
+ // optional
+ try {
+ callback.onReceiveResult(new SendMmsResult(SEND_STATUS_RETRY_ON_CARRIER_NETWORK, null));
+ } catch (RemoteException ex) {
+ }
+ }
+
+ /**
+ * Override this method to download MMSs received.
+ *
+ * @param contentUri the content provider URI of the PDU to be downloaded.
+ * @param subId SMS subscription ID of the SIM
+ * @param location the URI of the message to be downloaded.
+ * @param callback result callback. Call with a status code which is one of
+ * {@link #DOWNLOAD_STATUS_OK},
+ * {@link #DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK}, or {@link #DOWNLOAD_STATUS_ERROR}.
+ */
+ public void onDownloadMms(@NonNull Uri contentUri, int subId, @NonNull Uri location,
+ @NonNull ResultCallback<Integer> callback) {
+ // optional
+ try {
+ callback.onReceiveResult(DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK);
+ } catch (RemoteException ex) {
+ }
+ }
+
+ @Override
+ public @Nullable IBinder onBind(@NonNull Intent intent) {
+ if (!SERVICE_INTERFACE.equals(intent.getAction())) {
+ return null;
+ }
+ return mWrapper;
+ }
+
+ /**
+ * The result of sending an MMS.
+ */
+ public static final class SendMmsResult {
+ private int mSendStatus;
+ private byte[] mSendConfPdu;
+
+ /**
+ * Constructs a SendMmsResult with the MMS send result, and the SendConf PDU.
+ *
+ * @param sendStatus send status, one of {@link #SEND_STATUS_OK},
+ * {@link #SEND_STATUS_RETRY_ON_CARRIER_NETWORK}, and
+ * {@link #SEND_STATUS_ERROR}
+ * @param sendConfPdu a possibly {code null} SendConf PDU, which confirms that the message
+ * was sent. sendConfPdu is ignored if the {@code result} is not
+ * {@link #SEND_STATUS_OK}.
+ */
+ public SendMmsResult(int sendStatus, @Nullable byte[] sendConfPdu) {
+ mSendStatus = sendStatus;
+ mSendConfPdu = sendConfPdu;
+ }
+
+ /**
+ * Returns the send status of the just-sent MMS.
+ *
+ * @return the send status which is one of {@link #SEND_STATUS_OK},
+ * {@link #SEND_STATUS_RETRY_ON_CARRIER_NETWORK}, and {@link #SEND_STATUS_ERROR}
+ */
+ public int getSendStatus() {
+ return mSendStatus;
+ }
+
+ /**
+ * Returns the SendConf PDU, which confirms that the message was sent.
+ *
+ * @return the SendConf PDU
+ */
+ public @Nullable byte[] getSendConfPdu() {
+ return mSendConfPdu;
+ }
+ }
+
+ /**
+ * The result of sending an SMS.
+ */
+ public static final class SendSmsResult {
+ private final int mSendStatus;
+ private final int mMessageRef;
+
+ /**
+ * Constructs a SendSmsResult with the send status and message reference for the
+ * just-sent SMS.
+ *
+ * @param sendStatus send status, one of {@link #SEND_STATUS_OK},
+ * {@link #SEND_STATUS_RETRY_ON_CARRIER_NETWORK}, and {@link #SEND_STATUS_ERROR}.
+ * @param messageRef message reference of the just-sent SMS. This field is applicable only
+ * if send status is {@link #SEND_STATUS_OK}.
+ */
+ public SendSmsResult(int sendStatus, int messageRef) {
+ mSendStatus = sendStatus;
+ mMessageRef = messageRef;
+ }
+
+ /**
+ * Returns the message reference of the just-sent SMS.
+ *
+ * @return the message reference
+ */
+ public int getMessageRef() {
+ return mMessageRef;
+ }
+
+ /**
+ * Returns the send status of the just-sent SMS.
+ *
+ * @return the send status
+ */
+ public int getSendStatus() {
+ return mSendStatus;
+ }
+ }
+
+ /**
+ * The result of sending a multipart SMS.
+ */
+ public static final class SendMultipartSmsResult {
+ private final int mSendStatus;
+ private final int[] mMessageRefs;
+
+ /**
+ * Constructs a SendMultipartSmsResult with the send status and message references for the
+ * just-sent multipart SMS.
+ *
+ * @param sendStatus send status, one of {@link #SEND_STATUS_OK},
+ * {@link #SEND_STATUS_RETRY_ON_CARRIER_NETWORK}, and {@link #SEND_STATUS_ERROR}.
+ * @param messageRefs an array of message references, one for each part of the
+ * multipart SMS. This field is applicable only if send status is
+ * {@link #SEND_STATUS_OK}.
+ */
+ public SendMultipartSmsResult(int sendStatus, @Nullable int[] messageRefs) {
+ mSendStatus = sendStatus;
+ mMessageRefs = messageRefs;
+ }
+
+ /**
+ * Returns the message references of the just-sent multipart SMS.
+ *
+ * @return the message references, one for each part of the multipart SMS
+ */
+ public @Nullable int[] getMessageRefs() {
+ return mMessageRefs;
+ }
+
+ /**
+ * Returns the send status of the just-sent SMS.
+ *
+ * @return the send status
+ */
+ public int getSendStatus() {
+ return mSendStatus;
+ }
+ }
+
+ /**
+ * A callback interface used to provide results asynchronously.
+ */
+ public interface ResultCallback<T> {
+ /**
+ * Invoked when the result is available.
+ *
+ * @param result the result
+ */
+ public void onReceiveResult(@NonNull T result) throws RemoteException;
+ };
+
+ /**
+ * A wrapper around ICarrierMessagingService to enable the carrier messaging app to implement
+ * methods it cares about in the {@link ICarrierMessagingService} interface.
+ */
+ private class ICarrierMessagingWrapper extends ICarrierMessagingService.Stub {
+ @Override
+ public void filterSms(MessagePdu pdu, String format, int destPort,
+ int subId, final ICarrierMessagingCallback callback) {
+ onReceiveTextSms(pdu, format, destPort, subId,
+ new ResultCallback<Integer>() {
+ @Override
+ public void onReceiveResult(Integer options) throws RemoteException {
+ callback.onFilterComplete(options);
+ }
+ });
+ }
+
+ @Override
+ public void sendTextSms(String text, int subId, String destAddress,
+ int sendSmsFlag, final ICarrierMessagingCallback callback) {
+ onSendTextSms(text, subId, destAddress, sendSmsFlag,
+ new ResultCallback<SendSmsResult>() {
+ @Override
+ public void onReceiveResult(final SendSmsResult result) throws RemoteException {
+ callback.onSendSmsComplete(result.getSendStatus(), result.getMessageRef());
+ }
+ });
+ }
+
+ @Override
+ public void sendDataSms(byte[] data, int subId, String destAddress, int destPort,
+ int sendSmsFlag, final ICarrierMessagingCallback callback) {
+ onSendDataSms(data, subId, destAddress, destPort, sendSmsFlag,
+ new ResultCallback<SendSmsResult>() {
+ @Override
+ public void onReceiveResult(final SendSmsResult result) throws RemoteException {
+ callback.onSendSmsComplete(result.getSendStatus(), result.getMessageRef());
+ }
+ });
+ }
+
+ @Override
+ public void sendMultipartTextSms(List<String> parts, int subId, String destAddress,
+ int sendSmsFlag, final ICarrierMessagingCallback callback) {
+ onSendMultipartTextSms(parts, subId, destAddress, sendSmsFlag,
+ new ResultCallback<SendMultipartSmsResult>() {
+ @Override
+ public void onReceiveResult(final SendMultipartSmsResult result)
+ throws RemoteException {
+ callback.onSendMultipartSmsComplete(
+ result.getSendStatus(), result.getMessageRefs());
+ }
+ });
+ }
+
+ @Override
+ public void sendMms(Uri pduUri, int subId, Uri location,
+ final ICarrierMessagingCallback callback) {
+ onSendMms(pduUri, subId, location, new ResultCallback<SendMmsResult>() {
+ @Override
+ public void onReceiveResult(final SendMmsResult result) throws RemoteException {
+ callback.onSendMmsComplete(result.getSendStatus(), result.getSendConfPdu());
+ }
+ });
+ }
+
+ @Override
+ public void downloadMms(Uri pduUri, int subId, Uri location,
+ final ICarrierMessagingCallback callback) {
+ onDownloadMms(pduUri, subId, location, new ResultCallback<Integer>() {
+ @Override
+ public void onReceiveResult(Integer result) throws RemoteException {
+ callback.onDownloadMmsComplete(result);
+ }
+ });
+ }
+ }
+}
diff --git a/android/service/carrier/CarrierMessagingServiceWrapper.java b/android/service/carrier/CarrierMessagingServiceWrapper.java
new file mode 100644
index 0000000..2a809b1
--- /dev/null
+++ b/android/service/carrier/CarrierMessagingServiceWrapper.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2019 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.carrier;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * Provides basic structure for platform to connect to the carrier messaging service.
+ * <p>
+ * <code>
+ * CarrierMessagingServiceWrapper carrierMessagingServiceWrapper =
+ * new CarrierMessagingServiceWrapperImpl();
+ * if (carrierMessagingServiceWrapper.bindToCarrierMessagingService(context, carrierPackageName)) {
+ * // wait for onServiceReady callback
+ * } else {
+ * // Unable to bind: handle error.
+ * }
+ * </code>
+ * <p> Upon completion {@link #disposeConnection} should be called to unbind the
+ * CarrierMessagingService.
+ * @hide
+ */
+public abstract class CarrierMessagingServiceWrapper {
+ // Populated by bindToCarrierMessagingService. bindToCarrierMessagingService must complete
+ // prior to calling disposeConnection so that mCarrierMessagingServiceConnection is initialized.
+ private volatile CarrierMessagingServiceConnection mCarrierMessagingServiceConnection;
+
+ private volatile ICarrierMessagingService mICarrierMessagingService;
+
+ /**
+ * Binds to the carrier messaging service under package {@code carrierPackageName}. This method
+ * should be called exactly once.
+ *
+ * @param context the context
+ * @param carrierPackageName the carrier package name
+ * @return true upon successfully binding to a carrier messaging service, false otherwise
+ * @hide
+ */
+ public boolean bindToCarrierMessagingService(@NonNull Context context,
+ @NonNull String carrierPackageName) {
+ Preconditions.checkState(mCarrierMessagingServiceConnection == null);
+
+ Intent intent = new Intent(CarrierMessagingService.SERVICE_INTERFACE);
+ intent.setPackage(carrierPackageName);
+ mCarrierMessagingServiceConnection = new CarrierMessagingServiceConnection();
+ return context.bindService(intent, mCarrierMessagingServiceConnection,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ /**
+ * Unbinds the carrier messaging service. This method should be called exactly once.
+ *
+ * @param context the context
+ * @hide
+ */
+ public void disposeConnection(@NonNull Context context) {
+ Preconditions.checkNotNull(mCarrierMessagingServiceConnection);
+ context.unbindService(mCarrierMessagingServiceConnection);
+ mCarrierMessagingServiceConnection = null;
+ }
+
+ /**
+ * Implemented by subclasses to use the carrier messaging service once it is ready.
+ * @hide
+ */
+ public abstract void onServiceReady();
+
+ /**
+ * Called when connection with service is established.
+ *
+ * @param carrierMessagingService the carrier messaing service interface
+ */
+ private void onServiceReady(ICarrierMessagingService carrierMessagingService) {
+ mICarrierMessagingService = carrierMessagingService;
+ onServiceReady();
+ }
+
+ /**
+ * Request filtering an incoming SMS message.
+ * The service will call callback.onFilterComplete with the filtering result.
+ *
+ * @param pdu the PDUs of the message
+ * @param format the format of the PDUs, typically "3gpp" or "3gpp2"
+ * @param destPort the destination port of a data SMS. It will be -1 for text SMS
+ * @param subId SMS subscription ID of the SIM
+ * @param callback the callback to notify upon completion
+ * @hide
+ */
+ public void filterSms(@NonNull MessagePdu pdu, @NonNull String format, int destPort,
+ int subId, @NonNull final CarrierMessagingCallbackWrapper callback) {
+ if (mICarrierMessagingService != null) {
+ try {
+ mICarrierMessagingService.filterSms(pdu, format, destPort, subId,
+ new CarrierMessagingCallbackWrapperInternal(callback));
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Request sending a new text SMS from the device.
+ * The service will call {@link ICarrierMessagingCallback#onSendSmsComplete} with the send
+ * status.
+ *
+ * @param text the text to send
+ * @param subId SMS subscription ID of the SIM
+ * @param destAddress phone number of the recipient of the message
+ * @param sendSmsFlag flag for sending SMS
+ * @param callback the callback to notify upon completion
+ * @hide
+ */
+ public void sendTextSms(@NonNull String text, int subId, @NonNull String destAddress,
+ int sendSmsFlag, @NonNull final CarrierMessagingCallbackWrapper callback) {
+ if (mICarrierMessagingService != null) {
+ try {
+ mICarrierMessagingService.sendTextSms(text, subId, destAddress, sendSmsFlag,
+ new CarrierMessagingCallbackWrapperInternal(callback));
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Request sending a new data SMS from the device.
+ * The service will call {@link ICarrierMessagingCallback#onSendSmsComplete} with the send
+ * status.
+ *
+ * @param data the data to send
+ * @param subId SMS subscription ID of the SIM
+ * @param destAddress phone number of the recipient of the message
+ * @param destPort port number of the recipient of the message
+ * @param sendSmsFlag flag for sending SMS
+ * @param callback the callback to notify upon completion
+ * @hide
+ */
+ public void sendDataSms(@NonNull byte[] data, int subId, @NonNull String destAddress,
+ int destPort, int sendSmsFlag,
+ @NonNull final CarrierMessagingCallbackWrapper callback) {
+ if (mICarrierMessagingService != null) {
+ try {
+ mICarrierMessagingService.sendDataSms(data, subId, destAddress, destPort,
+ sendSmsFlag, new CarrierMessagingCallbackWrapperInternal(callback));
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Request sending a new multi-part text SMS from the device.
+ * The service will call {@link ICarrierMessagingCallback#onSendMultipartSmsComplete}
+ * with the send status.
+ *
+ * @param parts the parts of the multi-part text SMS to send
+ * @param subId SMS subscription ID of the SIM
+ * @param destAddress phone number of the recipient of the message
+ * @param sendSmsFlag flag for sending SMS
+ * @param callback the callback to notify upon completion
+ * @hide
+ */
+ public void sendMultipartTextSms(@NonNull List<String> parts, int subId,
+ @NonNull String destAddress, int sendSmsFlag,
+ @NonNull final CarrierMessagingCallbackWrapper callback) {
+ if (mICarrierMessagingService != null) {
+ try {
+ mICarrierMessagingService.sendMultipartTextSms(parts, subId, destAddress,
+ sendSmsFlag, new CarrierMessagingCallbackWrapperInternal(callback));
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Request sending a new MMS PDU from the device.
+ * The service will call {@link ICarrierMessagingCallback#onSendMmsComplete} with the send
+ * status.
+ *
+ * @param pduUri the content provider URI of the PDU to send
+ * @param subId SMS subscription ID of the SIM
+ * @param location the optional URI to send this MMS PDU. If this is {code null},
+ * the PDU should be sent to the default MMSC URL.
+ * @param callback the callback to notify upon completion
+ * @hide
+ */
+ public void sendMms(@NonNull Uri pduUri, int subId, @NonNull Uri location,
+ @NonNull final CarrierMessagingCallbackWrapper callback) {
+ if (mICarrierMessagingService != null) {
+ try {
+ mICarrierMessagingService.sendMms(pduUri, subId, location,
+ new CarrierMessagingCallbackWrapperInternal(callback));
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Request downloading a new MMS.
+ * The service will call {@link ICarrierMessagingCallback#onDownloadMmsComplete} with the
+ * download status.
+ *
+ * @param pduUri the content provider URI of the PDU to be downloaded.
+ * @param subId SMS subscription ID of the SIM
+ * @param location the URI of the message to be downloaded.
+ * @param callback the callback to notify upon completion
+ * @hide
+ */
+ public void downloadMms(@NonNull Uri pduUri, int subId, @NonNull Uri location,
+ @NonNull final CarrierMessagingCallbackWrapper callback) {
+ if (mICarrierMessagingService != null) {
+ try {
+ mICarrierMessagingService.downloadMms(pduUri, subId, location,
+ new CarrierMessagingCallbackWrapperInternal(callback));
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * A basic {@link ServiceConnection}.
+ */
+ private final class CarrierMessagingServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ onServiceReady(ICarrierMessagingService.Stub.asInterface(service));
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ }
+ }
+
+ /**
+ * Callback wrapper used for response to requests exposed by
+ * {@link CarrierMessagingServiceWrapper}.
+ * @hide
+ */
+ public abstract static class CarrierMessagingCallbackWrapper {
+
+ /**
+ * Response callback for {@link CarrierMessagingServiceWrapper#filterSms}.
+ * @param result a bitmask integer to indicate how the incoming text SMS should be handled
+ * by the platform. Bits set can be
+ * {@link CarrierMessagingService#RECEIVE_OPTIONS_DROP} and
+ * {@link CarrierMessagingService#
+ * RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE}.
+ * {@see CarrierMessagingService#onReceiveTextSms}.
+ * @hide
+ */
+ public void onFilterComplete(int result) {
+
+ }
+
+ /**
+ * Response callback for {@link CarrierMessagingServiceWrapper#sendTextSms} and
+ * {@link CarrierMessagingServiceWrapper#sendDataSms}.
+ * @param result send status, one of {@link CarrierMessagingService#SEND_STATUS_OK},
+ * {@link CarrierMessagingService#SEND_STATUS_RETRY_ON_CARRIER_NETWORK},
+ * and {@link CarrierMessagingService#SEND_STATUS_ERROR}.
+ * @param messageRef message reference of the just-sent message. This field is applicable
+ * only if result is {@link CarrierMessagingService#SEND_STATUS_OK}.
+ * @hide
+ */
+ public void onSendSmsComplete(int result, int messageRef) {
+
+ }
+
+ /**
+ * Response callback for {@link CarrierMessagingServiceWrapper#sendMultipartTextSms}.
+ * @param result send status, one of {@link CarrierMessagingService#SEND_STATUS_OK},
+ * {@link CarrierMessagingService#SEND_STATUS_RETRY_ON_CARRIER_NETWORK},
+ * and {@link CarrierMessagingService#SEND_STATUS_ERROR}.
+ * @param messageRefs an array of message references, one for each part of the
+ * multipart SMS. This field is applicable only if result is
+ * {@link CarrierMessagingService#SEND_STATUS_OK}.
+ * @hide
+ */
+ public void onSendMultipartSmsComplete(int result, @Nullable int[] messageRefs) {
+
+ }
+
+ /**
+ * Response callback for {@link CarrierMessagingServiceWrapper#sendMms}.
+ * @param result send status, one of {@link CarrierMessagingService#SEND_STATUS_OK},
+ * {@link CarrierMessagingService#SEND_STATUS_RETRY_ON_CARRIER_NETWORK},
+ * and {@link CarrierMessagingService#SEND_STATUS_ERROR}.
+ * @param sendConfPdu a possibly {code null} SendConf PDU, which confirms that the message
+ * was sent. sendConfPdu is ignored if the {@code result} is not
+ * {@link CarrierMessagingService#SEND_STATUS_OK}.
+ * @hide
+ */
+ public void onSendMmsComplete(int result, @Nullable byte[] sendConfPdu) {
+
+ }
+
+ /**
+ * Response callback for {@link CarrierMessagingServiceWrapper#downloadMms}.
+ * @param result download status, one of {@link CarrierMessagingService#SEND_STATUS_OK},
+ * {@link CarrierMessagingService#SEND_STATUS_RETRY_ON_CARRIER_NETWORK},
+ * and {@link CarrierMessagingService#SEND_STATUS_ERROR}.
+ * @hide
+ */
+ public void onDownloadMmsComplete(int result) {
+
+ }
+ }
+
+ private final class CarrierMessagingCallbackWrapperInternal
+ extends ICarrierMessagingCallback.Stub {
+ CarrierMessagingCallbackWrapper mCarrierMessagingCallbackWrapper;
+
+ CarrierMessagingCallbackWrapperInternal(CarrierMessagingCallbackWrapper callback) {
+ mCarrierMessagingCallbackWrapper = callback;
+ }
+
+ @Override
+ public void onFilterComplete(int result) throws RemoteException {
+ mCarrierMessagingCallbackWrapper.onFilterComplete(result);
+ }
+
+ @Override
+ public void onSendSmsComplete(int result, int messageRef) throws RemoteException {
+ mCarrierMessagingCallbackWrapper.onSendSmsComplete(result, messageRef);
+ }
+
+ @Override
+ public void onSendMultipartSmsComplete(int result, int[] messageRefs)
+ throws RemoteException {
+ mCarrierMessagingCallbackWrapper.onSendMultipartSmsComplete(result, messageRefs);
+ }
+
+ @Override
+ public void onSendMmsComplete(int result, byte[] sendConfPdu) throws RemoteException {
+ mCarrierMessagingCallbackWrapper.onSendMmsComplete(result, sendConfPdu);
+ }
+
+ @Override
+ public void onDownloadMmsComplete(int result) throws RemoteException {
+ mCarrierMessagingCallbackWrapper.onDownloadMmsComplete(result);
+ }
+ }
+}
diff --git a/android/service/carrier/CarrierService.java b/android/service/carrier/CarrierService.java
new file mode 100644
index 0000000..d06ec11
--- /dev/null
+++ b/android/service/carrier/CarrierService.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2015 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.carrier;
+
+import android.annotation.CallSuper;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.os.ResultReceiver;
+import android.telephony.TelephonyRegistryManager;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * A service that exposes carrier-specific functionality to the system.
+ * <p>
+ * To extend this class, you must declare the service in your manifest file to require the
+ * {@link android.Manifest.permission#BIND_CARRIER_SERVICES} permission and include an intent
+ * filter with the {@link #CARRIER_SERVICE_INTERFACE}. If the service should have a long-lived
+ * binding, set <code>android.service.carrier.LONG_LIVED_BINDING</code> to <code>true</code> in the
+ * service's metadata. For example:
+ * </p>
+ *
+ * <pre>{@code
+ * <service android:name=".MyCarrierService"
+ * android:label="@string/service_name"
+ * android:permission="android.permission.BIND_CARRIER_SERVICES">
+ * <intent-filter>
+ * <action android:name="android.service.carrier.CarrierService" />
+ * </intent-filter>
+ * <meta-data android:name="android.service.carrier.LONG_LIVED_BINDING"
+ * android:value="true" />
+ * </service>
+ * }</pre>
+ */
+public abstract class CarrierService extends Service {
+
+ private static final String LOG_TAG = "CarrierService";
+
+ public static final String CARRIER_SERVICE_INTERFACE = "android.service.carrier.CarrierService";
+
+ private final ICarrierService.Stub mStubWrapper;
+
+ public CarrierService() {
+ mStubWrapper = new ICarrierServiceWrapper();
+ }
+
+ /**
+ * Override this method to set carrier configuration.
+ * <p>
+ * This method will be called by telephony services to get carrier-specific configuration
+ * values. The returned config will be saved by the system until,
+ * <ol>
+ * <li>The carrier app package is updated, or</li>
+ * <li>The carrier app requests a reload with
+ * {@link android.telephony.CarrierConfigManager#notifyConfigChangedForSubId
+ * notifyConfigChangedForSubId}.</li>
+ * </ol>
+ * This method can be called after a SIM card loads, which may be before or after boot.
+ * </p>
+ * <p>
+ * This method should not block for a long time. If expensive operations (e.g. network access)
+ * are required, this method can schedule the work and return null. Then, use
+ * {@link android.telephony.CarrierConfigManager#notifyConfigChangedForSubId
+ * notifyConfigChangedForSubId} to trigger a reload when the config is ready.
+ * </p>
+ * <p>
+ * Implementations should use the keys defined in {@link android.telephony.CarrierConfigManager
+ * CarrierConfigManager}. Any configuration values not set in the returned {@link
+ * PersistableBundle} may be overridden by the system's default configuration service.
+ * </p>
+ *
+ * @param id contains details about the current carrier that can be used do decide what
+ * configuration values to return. Instead of using details like MCCMNC to decide
+ * current carrier, it also contains subscription carrier id
+ * {@link android.telephony.TelephonyManager#getSimCarrierId()}, a platform-wide
+ * unique identifier for each carrier, CarrierConfigService can directly use carrier
+ * id as the key to look up the carrier info.
+ * @return a {@link PersistableBundle} object containing the configuration or null if default
+ * values should be used.
+ */
+ public abstract PersistableBundle onLoadConfig(CarrierIdentifier id);
+
+ /**
+ * Informs the system of an intentional upcoming carrier network change by
+ * a carrier app. This call is optional and is only used to allow the
+ * system to provide alternative UI while telephony is performing an action
+ * that may result in intentional, temporary network lack of connectivity.
+ * <p>
+ * Based on the active parameter passed in, this method will either show or
+ * hide the alternative UI. There is no timeout associated with showing
+ * this UX, so a carrier app must be sure to call with active set to false
+ * sometime after calling with it set to true.
+ * <p>
+ * Requires Permission: calling app has carrier privileges.
+ *
+ * @param active Whether the carrier network change is or shortly will be
+ * active. Set this value to true to begin showing
+ * alternative UI and false to stop.
+ * @see android.telephony.TelephonyManager#hasCarrierPrivileges
+ */
+ public final void notifyCarrierNetworkChange(boolean active) {
+ TelephonyRegistryManager telephonyRegistryMgr =
+ (TelephonyRegistryManager) this.getSystemService(
+ Context.TELEPHONY_REGISTRY_SERVICE);
+ if (telephonyRegistryMgr != null) {
+ telephonyRegistryMgr.notifyCarrierNetworkChange(active);
+ }
+ }
+
+ /**
+ * If overriding this method, call through to the super method for any unknown actions.
+ * {@inheritDoc}
+ */
+ @Override
+ @CallSuper
+ public IBinder onBind(Intent intent) {
+ return mStubWrapper;
+ }
+
+ /**
+ * A wrapper around ICarrierService that forwards calls to implementations of
+ * {@link CarrierService}.
+ * @hide
+ */
+ public class ICarrierServiceWrapper extends ICarrierService.Stub {
+ /** @hide */
+ public static final int RESULT_OK = 0;
+ /** @hide */
+ public static final int RESULT_ERROR = 1;
+ /** @hide */
+ public static final String KEY_CONFIG_BUNDLE = "config_bundle";
+
+ @Override
+ public void getCarrierConfig(CarrierIdentifier id, ResultReceiver result) {
+ try {
+ Bundle data = new Bundle();
+ data.putParcelable(KEY_CONFIG_BUNDLE, CarrierService.this.onLoadConfig(id));
+ result.send(RESULT_OK, data);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Error in onLoadConfig: " + e.getMessage(), e);
+ result.send(RESULT_ERROR, null);
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ CarrierService.this.dump(fd, pw, args);
+ }
+ }
+}
diff --git a/android/service/carrier/MessagePdu.java b/android/service/carrier/MessagePdu.java
new file mode 100644
index 0000000..19c41b1
--- /dev/null
+++ b/android/service/carrier/MessagePdu.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2014 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.carrier;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A parcelable list of PDUs representing contents of a possibly multi-part SMS.
+ */
+public final class MessagePdu implements Parcelable {
+ private static final int NULL_LENGTH = -1;
+
+ private final List<byte[]> mPduList;
+
+ /**
+ * Constructs a MessagePdu with the list of message PDUs.
+ *
+ * @param pduList the list of message PDUs
+ */
+ public MessagePdu(@NonNull List<byte[]> pduList) {
+ if (pduList == null || pduList.contains(null)) {
+ throw new IllegalArgumentException("pduList must not be null or contain nulls");
+ }
+ mPduList = pduList;
+ }
+
+ /**
+ * Returns the contents of a possibly multi-part SMS.
+ *
+ * @return the list of PDUs
+ */
+ public @NonNull List<byte[]> getPdus() {
+ return mPduList;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mPduList == null) {
+ dest.writeInt(NULL_LENGTH);
+ } else {
+ dest.writeInt(mPduList.size());
+ for (byte[] messagePdu : mPduList) {
+ dest.writeByteArray(messagePdu);
+ }
+ }
+ }
+
+ /**
+ * Constructs a {@link MessagePdu} from a {@link Parcel}.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<MessagePdu> CREATOR
+ = new Parcelable.Creator<MessagePdu>() {
+ @Override
+ public MessagePdu createFromParcel(Parcel source) {
+ int size = source.readInt();
+ List<byte[]> pduList;
+ if (size == NULL_LENGTH) {
+ pduList = null;
+ } else {
+ pduList = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ pduList.add(source.createByteArray());
+ }
+ }
+ return new MessagePdu(pduList);
+ }
+
+ @Override
+ public MessagePdu[] newArray(int size) {
+ return new MessagePdu[size];
+ }
+ };
+}
diff --git a/android/service/chooser/ChooserTarget.java b/android/service/chooser/ChooserTarget.java
new file mode 100644
index 0000000..ec3dfe1
--- /dev/null
+++ b/android/service/chooser/ChooserTarget.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2015 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.chooser;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A ChooserTarget represents a deep-link into an application as returned by a
+ * {@link android.service.chooser.ChooserTargetService}.
+ *
+ * <p>A chooser target represents a specific deep link target into an application exposed
+ * for selection by the user. This might be a frequently emailed contact, a recently active
+ * group messaging conversation, a folder in a cloud storage app, a collection of related
+ * items published on a social media service or any other contextually relevant grouping
+ * of target app + relevant metadata.</p>
+ *
+ * <p>Creators of chooser targets should consult the relevant design guidelines for the type
+ * of target they are presenting. For example, targets involving people should be presented
+ * with a circular icon.</p>
+ *
+ * @deprecated For publishing direct share targets, please follow the instructions in
+ * https://developer.android.com/training/sharing/receive.html#providing-direct-share-targets
+ * instead.
+ */
+@Deprecated
+public final class ChooserTarget implements Parcelable {
+ private static final String TAG = "ChooserTarget";
+
+ /**
+ * The title of this target that will be shown to the user. The title may be truncated
+ * if it is too long to display in the space provided.
+ */
+ private CharSequence mTitle;
+
+ /**
+ * The icon that will be shown to the user to represent this target.
+ * The system may resize this icon as appropriate.
+ */
+ private Icon mIcon;
+
+ /**
+ * The ComponentName of the Activity to be invoked. Must be part of the target creator's
+ * own package or an Activity exported by its package.
+ */
+ private ComponentName mComponentName;
+
+ /**
+ * A Bundle to merge with the extras of the intent sent to this target.
+ * Any extras here will override the extras from the original intent.
+ */
+ private Bundle mIntentExtras;
+
+ /**
+ * The score given to this item. It can be normalized.
+ */
+ private float mScore;
+
+ /**
+ * Construct a deep link target for presentation by a chooser UI.
+ *
+ * <p>A target is composed of a title and an icon for presentation to the user.
+ * The UI presenting this target may truncate the title if it is too long to be presented
+ * in the available space, as well as crop, resize or overlay the supplied icon.</p>
+ *
+ * <p>The creator of a target may supply a ranking score. This score is assumed to be relative
+ * to the other targets supplied by the same
+ * {@link ChooserTargetService#onGetChooserTargets(ComponentName, IntentFilter) query}.
+ * Scores should be in the range from 0.0f (unlikely match) to 1.0f (very relevant match).
+ * Scores for a set of targets do not need to sum to 1.</p>
+ *
+ * <p>The ComponentName must be the name of an Activity component in the creator's own
+ * package, or an exported component from any other package. You may provide an optional
+ * Bundle of extras that will be merged into the final intent before it is sent to the
+ * target Activity; use this to add any additional data about the deep link that the target
+ * activity will read. e.g. conversation IDs, email addresses, etc.</p>
+ *
+ * <p>Take care not to place custom {@link android.os.Parcelable} types into
+ * the extras bundle, as the system will not be able to unparcel them to merge them.</p>
+ *
+ * @param title title of this target that will be shown to a user
+ * @param icon icon to represent this target
+ * @param score ranking score for this target between 0.0f and 1.0f, inclusive
+ * @param componentName Name of the component to be launched if this target is chosen
+ * @param intentExtras Bundle of extras to merge with the extras of the launched intent
+ */
+ public ChooserTarget(CharSequence title, Icon icon, float score,
+ ComponentName componentName, @Nullable Bundle intentExtras) {
+ mTitle = title;
+ mIcon = icon;
+ if (score > 1.f || score < 0.f) {
+ throw new IllegalArgumentException("Score " + score + " out of range; "
+ + "must be between 0.0f and 1.0f");
+ }
+ mScore = score;
+ mComponentName = componentName;
+ mIntentExtras = intentExtras;
+ }
+
+ ChooserTarget(Parcel in) {
+ mTitle = in.readCharSequence();
+ if (in.readInt() != 0) {
+ mIcon = Icon.CREATOR.createFromParcel(in);
+ } else {
+ mIcon = null;
+ }
+ mScore = in.readFloat();
+ mComponentName = ComponentName.readFromParcel(in);
+ mIntentExtras = in.readBundle();
+ }
+
+ /**
+ * Returns the title of this target for display to a user. The UI displaying the title
+ * may truncate this title if it is too long to be displayed in full.
+ *
+ * @return the title of this target, intended to be shown to a user
+ */
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the icon representing this target for display to a user. The UI displaying the icon
+ * may crop, resize or overlay this icon.
+ *
+ * @return the icon representing this target, intended to be shown to a user
+ */
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * Returns the ranking score supplied by the creator of this ChooserTarget.
+ * Values are between 0.0f and 1.0f. The UI displaying the target may
+ * take this score into account when sorting and merging targets from multiple sources.
+ *
+ * @return the ranking score for this target between 0.0f and 1.0f, inclusive
+ */
+ public float getScore() {
+ return mScore;
+ }
+
+ /**
+ * Returns the ComponentName of the Activity that should be launched for this ChooserTarget.
+ *
+ * @return the name of the target Activity to launch
+ */
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ /**
+ * Returns the Bundle of extras to be added to an intent launched to this target.
+ *
+ * @return the extras to merge with the extras of the intent being launched
+ */
+ public Bundle getIntentExtras() {
+ return mIntentExtras;
+ }
+
+ @Override
+ public String toString() {
+ return "ChooserTarget{"
+ + mComponentName
+ + ", " + mIntentExtras
+ + ", '" + mTitle
+ + "', " + mScore + "}";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeCharSequence(mTitle);
+ if (mIcon != null) {
+ dest.writeInt(1);
+ mIcon.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeFloat(mScore);
+ ComponentName.writeToParcel(mComponentName, dest);
+ dest.writeBundle(mIntentExtras);
+ }
+
+ public static final @android.annotation.NonNull Creator<ChooserTarget> CREATOR
+ = new Creator<ChooserTarget>() {
+ @Override
+ public ChooserTarget createFromParcel(Parcel source) {
+ return new ChooserTarget(source);
+ }
+
+ @Override
+ public ChooserTarget[] newArray(int size) {
+ return new ChooserTarget[size];
+ }
+ };
+}
diff --git a/android/service/chooser/ChooserTargetService.java b/android/service/chooser/ChooserTargetService.java
new file mode 100644
index 0000000..ec56064
--- /dev/null
+++ b/android/service/chooser/ChooserTargetService.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2015 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.chooser;
+
+import android.annotation.SdkConstant;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * A service that receives calls from the system when the user is asked to choose
+ * a target for an intent explicitly by another app. The calling app must have invoked
+ * {@link android.content.Intent#ACTION_CHOOSER ACTION_CHOOSER} as handled by the system;
+ * applications do not have the ability to query a ChooserTargetService directly.
+ *
+ * <p>Which ChooserTargetServices are queried depends on a system-level policy decision
+ * made at the moment the chooser is invoked, including but not limited to user time
+ * spent with the app package or associated components in the foreground, recency of usage
+ * or frequency of usage. These will generally correlate with the order that app targets
+ * are shown in the list of intent handlers shown in the system chooser or resolver.</p>
+ *
+ * <p>To extend this class, you must declare the service in your manifest file with
+ * the {@link android.Manifest.permission#BIND_CHOOSER_TARGET_SERVICE} permission
+ * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
+ * <pre>
+ * <service android:name=".MyChooserTargetService"
+ * android:label="@string/service_name"
+ * android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.chooser.ChooserTargetService" />
+ * </intent-filter>
+ * </service>
+ * </pre>
+ *
+ * <p>For the system to query your service, you must add a <meta-data> element to the
+ * Activity in your manifest that can handle Intents that you would also like to provide
+ * optional deep links for. For example, a chat app might offer deep links to recent active
+ * conversations instead of invoking a generic picker after the app itself is chosen as a target.
+ * </p>
+ *
+ * <p>The meta-data element should have the name
+ * <code>android.service.chooser.chooser_target_service</code> and a value corresponding to
+ * the component name of your service. Example:</p>
+ * <pre>
+ * <activity android:name=".MyShareActivity"
+ * android:label="@string/share_activity_label">
+ * <intent-filter>
+ * <action android:name="android.intent.action.SEND" />
+ * </intent-filter>
+ * <meta-data android:name="android.service.chooser.chooser_target_service"
+ * android:value=".MyChooserTargetService" />
+ * </activity>
+ * </pre>
+ *
+ * @deprecated For publishing direct share targets, please follow the instructions in
+ * https://developer.android.com/training/sharing/receive.html#providing-direct-share-targets
+ * instead.
+ */
+
+@Deprecated
+public abstract class ChooserTargetService extends Service {
+ // TAG = "ChooserTargetService[MySubclass]";
+ private final String TAG = ChooserTargetService.class.getSimpleName()
+ + '[' + getClass().getSimpleName() + ']';
+
+ private static final boolean DEBUG = false;
+
+ /**
+ * The Intent action that a ChooserTargetService must respond to
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.service.chooser.ChooserTargetService";
+
+ /**
+ * The name of the <code>meta-data</code> element that must be present on an
+ * <code>activity</code> element in a manifest to link it to a ChooserTargetService
+ */
+ public static final String META_DATA_NAME = "android.service.chooser.chooser_target_service";
+
+ /**
+ * The permission that a ChooserTargetService must require in order to bind to it.
+ * If this permission is not enforced the system will skip that ChooserTargetService.
+ */
+ public static final String BIND_PERMISSION = "android.permission.BIND_CHOOSER_TARGET_SERVICE";
+
+ private IChooserTargetServiceWrapper mWrapper = null;
+
+ /**
+ * Called by the system to retrieve a set of deep-link {@link ChooserTarget targets} that
+ * can handle an intent.
+ *
+ * <p>The returned list should be sorted such that the most relevant targets appear first.
+ * The score for each ChooserTarget will be combined with the system's score for the original
+ * target Activity to sort and filter targets presented to the user.</p>
+ *
+ * <p><em>Important:</em> Calls to this method from other applications will occur on
+ * a binder thread, not on your app's main thread. Make sure that access to relevant data
+ * within your app is thread-safe.</p>
+ *
+ * @param targetActivityName the ComponentName of the matched activity that referred the system
+ * to this ChooserTargetService
+ * @param matchedFilter the specific IntentFilter on the component that was matched
+ * @return a list of deep-link targets to fulfill the intent match, sorted by relevance
+ */
+ public abstract List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName,
+ IntentFilter matchedFilter);
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (DEBUG) Log.d(TAG, "onBind " + intent);
+ if (!SERVICE_INTERFACE.equals(intent.getAction())) {
+ if (DEBUG) Log.d(TAG, "bad intent action " + intent.getAction() + "; returning null");
+ return null;
+ }
+
+ if (mWrapper == null) {
+ mWrapper = new IChooserTargetServiceWrapper();
+ }
+ return mWrapper;
+ }
+
+ private class IChooserTargetServiceWrapper extends IChooserTargetService.Stub {
+ @Override
+ public void getChooserTargets(ComponentName targetComponentName,
+ IntentFilter matchedFilter, IChooserTargetResult result) throws RemoteException {
+ List<ChooserTarget> targets = null;
+ final long id = clearCallingIdentity();
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "getChooserTargets calling onGetChooserTargets; "
+ + targetComponentName + " filter: " + matchedFilter);
+ }
+ targets = onGetChooserTargets(targetComponentName, matchedFilter);
+ } finally {
+ restoreCallingIdentity(id);
+ result.sendResult(targets);
+ if (DEBUG) Log.d(TAG, "Sent results");
+ }
+ }
+ }
+}
diff --git a/android/service/contentcapture/ActivityEvent.java b/android/service/contentcapture/ActivityEvent.java
new file mode 100644
index 0000000..b741cff
--- /dev/null
+++ b/android/service/contentcapture/ActivityEvent.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2019 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.contentcapture;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.usage.UsageEvents.Event;
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents an activity-level event that is not associated with a session.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class ActivityEvent implements Parcelable {
+
+ /**
+ * The activity resumed.
+ */
+ public static final int TYPE_ACTIVITY_RESUMED = Event.ACTIVITY_RESUMED;
+
+ /**
+ * The activity paused.
+ */
+ public static final int TYPE_ACTIVITY_PAUSED = Event.ACTIVITY_PAUSED;
+
+ /**
+ * The activity stopped.
+ */
+ public static final int TYPE_ACTIVITY_STOPPED = Event.ACTIVITY_STOPPED;
+
+ /**
+ * The activity was destroyed.
+ */
+ public static final int TYPE_ACTIVITY_DESTROYED = Event.ACTIVITY_DESTROYED;
+
+ /** @hide */
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_ACTIVITY_RESUMED,
+ TYPE_ACTIVITY_PAUSED,
+ TYPE_ACTIVITY_STOPPED,
+ TYPE_ACTIVITY_DESTROYED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActivityEventType{}
+
+ private final @NonNull ComponentName mComponentName;
+ private final @ActivityEventType int mType;
+
+ /** @hide */
+ public ActivityEvent(@NonNull ComponentName componentName, @ActivityEventType int type) {
+ mComponentName = componentName;
+ mType = type;
+ }
+
+ /**
+ * Gests the {@link ComponentName} of the activity associated with the event.
+ */
+ @NonNull
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ /**
+ * Gets the event type.
+ *
+ * @return either {@link #TYPE_ACTIVITY_RESUMED}, {@value #TYPE_ACTIVITY_PAUSED},
+ * {@value #TYPE_ACTIVITY_STOPPED}, or {@value #TYPE_ACTIVITY_DESTROYED}.
+ */
+ @ActivityEventType
+ public int getEventType() {
+ return mType;
+ }
+
+ /** @hide */
+ public static String getTypeAsString(@ActivityEventType int type) {
+ switch (type) {
+ case TYPE_ACTIVITY_RESUMED:
+ return "ACTIVITY_RESUMED";
+ case TYPE_ACTIVITY_PAUSED:
+ return "ACTIVITY_PAUSED";
+ case TYPE_ACTIVITY_STOPPED:
+ return "ACTIVITY_STOPPED";
+ case TYPE_ACTIVITY_DESTROYED:
+ return "ACTIVITY_DESTROYED";
+ default:
+ return "UKNOWN_TYPE: " + type;
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return new StringBuilder("ActivityEvent[").append(mComponentName.toShortString())
+ .append("]:").append(getTypeAsString(mType)).toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel parcel, int flags) {
+ parcel.writeParcelable(mComponentName, flags);
+ parcel.writeInt(mType);
+ }
+
+ public static final @android.annotation.NonNull Creator<ActivityEvent> CREATOR =
+ new Creator<ActivityEvent>() {
+
+ @Override
+ @NonNull
+ public ActivityEvent createFromParcel(@NonNull Parcel parcel) {
+ final ComponentName componentName = parcel.readParcelable(null);
+ final int eventType = parcel.readInt();
+ return new ActivityEvent(componentName, eventType);
+ }
+
+ @Override
+ @NonNull
+ public ActivityEvent[] newArray(int size) {
+ return new ActivityEvent[size];
+ }
+ };
+}
diff --git a/android/service/contentcapture/ContentCaptureService.java b/android/service/contentcapture/ContentCaptureService.java
new file mode 100644
index 0000000..46cb65b
--- /dev/null
+++ b/android/service/contentcapture/ContentCaptureService.java
@@ -0,0 +1,766 @@
+/*
+ * Copyright (C) 2018 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.contentcapture;
+
+import static android.view.contentcapture.ContentCaptureHelper.sDebug;
+import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
+import static android.view.contentcapture.ContentCaptureHelper.toList;
+import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.ContentCaptureOptions;
+import android.content.Intent;
+import android.content.pm.ParceledListSlice;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseIntArray;
+import android.view.contentcapture.ContentCaptureCondition;
+import android.view.contentcapture.ContentCaptureContext;
+import android.view.contentcapture.ContentCaptureEvent;
+import android.view.contentcapture.ContentCaptureManager;
+import android.view.contentcapture.ContentCaptureSession;
+import android.view.contentcapture.ContentCaptureSessionId;
+import android.view.contentcapture.DataRemovalRequest;
+import android.view.contentcapture.DataShareRequest;
+import android.view.contentcapture.IContentCaptureDirectManager;
+import android.view.contentcapture.MainContentCaptureSession;
+
+import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.Preconditions;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * A service used to capture the content of the screen to provide contextual data in other areas of
+ * the system such as Autofill.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public abstract class ContentCaptureService extends Service {
+
+ private static final String TAG = ContentCaptureService.class.getSimpleName();
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ *
+ * <p>To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_CONTENT_CAPTURE_SERVICE} permission so
+ * that other applications can not abuse it.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.contentcapture.ContentCaptureService";
+
+ /**
+ * Name under which a ContentCaptureService component publishes information about itself.
+ *
+ * <p>This meta-data should reference an XML resource containing a
+ * <code><{@link
+ * android.R.styleable#ContentCaptureService content-capture-service}></code> tag.
+ *
+ * <p>Here's an example of how to use it on {@code AndroidManifest.xml}:
+ *
+ * <pre>
+ * <service android:name=".MyContentCaptureService"
+ * android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.contentcapture.ContentCaptureService" />
+ * </intent-filter>
+ *
+ * <meta-data
+ * android:name="android.content_capture"
+ * android:resource="@xml/my_content_capture_service"/>
+ * </service>
+ * </pre>
+ *
+ * <p>And then on {@code res/xml/my_content_capture_service.xml}:
+ *
+ * <pre>
+ * <content-capture-service xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:settingsActivity="my.package.MySettingsActivity">
+ * </content-capture-service>
+ * </pre>
+ */
+ public static final String SERVICE_META_DATA = "android.content_capture";
+
+ private final LocalDataShareAdapterResourceManager mDataShareAdapterResourceManager =
+ new LocalDataShareAdapterResourceManager();
+
+ private Handler mHandler;
+ private IContentCaptureServiceCallback mCallback;
+
+ private long mCallerMismatchTimeout = 1000;
+ private long mLastCallerMismatchLog;
+
+ /**
+ * Binder that receives calls from the system server.
+ */
+ private final IContentCaptureService mServerInterface = new IContentCaptureService.Stub() {
+
+ @Override
+ public void onConnected(IBinder callback, boolean verbose, boolean debug) {
+ sVerbose = verbose;
+ sDebug = debug;
+ mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnConnected,
+ ContentCaptureService.this, callback));
+ }
+
+ @Override
+ public void onDisconnected() {
+ mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDisconnected,
+ ContentCaptureService.this));
+ }
+
+ @Override
+ public void onSessionStarted(ContentCaptureContext context, int sessionId, int uid,
+ IResultReceiver clientReceiver, int initialState) {
+ mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnCreateSession,
+ ContentCaptureService.this, context, sessionId, uid, clientReceiver,
+ initialState));
+ }
+
+ @Override
+ public void onActivitySnapshot(int sessionId, SnapshotData snapshotData) {
+ mHandler.sendMessage(
+ obtainMessage(ContentCaptureService::handleOnActivitySnapshot,
+ ContentCaptureService.this, sessionId, snapshotData));
+ }
+
+ @Override
+ public void onSessionFinished(int sessionId) {
+ mHandler.sendMessage(obtainMessage(ContentCaptureService::handleFinishSession,
+ ContentCaptureService.this, sessionId));
+ }
+
+ @Override
+ public void onDataRemovalRequest(DataRemovalRequest request) {
+ mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDataRemovalRequest,
+ ContentCaptureService.this, request));
+ }
+
+ @Override
+ public void onDataShared(DataShareRequest request, IDataShareCallback callback) {
+ mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDataShared,
+ ContentCaptureService.this, request, callback));
+ }
+
+ @Override
+ public void onActivityEvent(ActivityEvent event) {
+ mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnActivityEvent,
+ ContentCaptureService.this, event));
+ }
+ };
+
+ /**
+ * Binder that receives calls from the app.
+ */
+ private final IContentCaptureDirectManager mClientInterface =
+ new IContentCaptureDirectManager.Stub() {
+
+ @Override
+ public void sendEvents(@SuppressWarnings("rawtypes") ParceledListSlice events, int reason,
+ ContentCaptureOptions options) {
+ mHandler.sendMessage(obtainMessage(ContentCaptureService::handleSendEvents,
+ ContentCaptureService.this, Binder.getCallingUid(), events, reason, options));
+ }
+ };
+
+ /**
+ * UIDs associated with each session.
+ *
+ * <p>This map is populated when an session is started, which is called by the system server
+ * and can be trusted. Then subsequent calls made by the app are verified against this map.
+ */
+ private final SparseIntArray mSessionUids = new SparseIntArray();
+
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler(Looper.getMainLooper(), null, true);
+ }
+
+ /** @hide */
+ @Override
+ public final IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mServerInterface.asBinder();
+ }
+ Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+ return null;
+ }
+
+ /**
+ * Explicitly limits content capture to the given packages and activities.
+ *
+ * <p>To reset the whitelist, call it passing {@code null} to both arguments.
+ *
+ * <p>Useful when the service wants to restrict content capture to a category of apps, like
+ * chat apps. For example, if the service wants to support view captures on all activities of
+ * app {@code ChatApp1} and just activities {@code act1} and {@code act2} of {@code ChatApp2},
+ * it would call: {@code setContentCaptureWhitelist(Sets.newArraySet("ChatApp1"),
+ * Sets.newArraySet(new ComponentName("ChatApp2", "act1"),
+ * new ComponentName("ChatApp2", "act2")));}
+ */
+ public final void setContentCaptureWhitelist(@Nullable Set<String> packages,
+ @Nullable Set<ComponentName> activities) {
+ final IContentCaptureServiceCallback callback = mCallback;
+ if (callback == null) {
+ Log.w(TAG, "setContentCaptureWhitelist(): no server callback");
+ return;
+ }
+
+ try {
+ callback.setContentCaptureWhitelist(toList(packages), toList(activities));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Explicitly sets the conditions for which content capture should be available by an app.
+ *
+ * <p>Typically used to restrict content capture to a few websites on browser apps. Example:
+ *
+ * <code>
+ * ArraySet<ContentCaptureCondition> conditions = new ArraySet<>(1);
+ * conditions.add(new ContentCaptureCondition(new LocusId("^https://.*\\.example\\.com$"),
+ * ContentCaptureCondition.FLAG_IS_REGEX));
+ * service.setContentCaptureConditions("com.example.browser_app", conditions);
+ *
+ * </code>
+ *
+ * <p>NOTE: </p> this method doesn't automatically disable content capture for the given
+ * conditions; it's up to the {@code packageName} implementation to call
+ * {@link ContentCaptureManager#getContentCaptureConditions()} and disable it accordingly.
+ *
+ * @param packageName name of the packages where the restrictions are set.
+ * @param conditions list of conditions, or {@code null} to reset the conditions for the
+ * package.
+ */
+ public final void setContentCaptureConditions(@NonNull String packageName,
+ @Nullable Set<ContentCaptureCondition> conditions) {
+ final IContentCaptureServiceCallback callback = mCallback;
+ if (callback == null) {
+ Log.w(TAG, "setContentCaptureConditions(): no server callback");
+ return;
+ }
+
+ try {
+ callback.setContentCaptureConditions(packageName, toList(conditions));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called when the Android system connects to service.
+ *
+ * <p>You should generally do initialization here rather than in {@link #onCreate}.
+ */
+ public void onConnected() {
+ Slog.i(TAG, "bound to " + getClass().getName());
+ }
+
+ /**
+ * Creates a new content capture session.
+ *
+ * @param context content capture context
+ * @param sessionId the session's Id
+ */
+ public void onCreateContentCaptureSession(@NonNull ContentCaptureContext context,
+ @NonNull ContentCaptureSessionId sessionId) {
+ if (sVerbose) {
+ Log.v(TAG, "onCreateContentCaptureSession(id=" + sessionId + ", ctx=" + context + ")");
+ }
+ }
+
+ /**
+ * Notifies the service of {@link ContentCaptureEvent events} associated with a content capture
+ * session.
+ *
+ * @param sessionId the session's Id
+ * @param event the event
+ */
+ public void onContentCaptureEvent(@NonNull ContentCaptureSessionId sessionId,
+ @NonNull ContentCaptureEvent event) {
+ if (sVerbose) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")");
+ }
+
+ /**
+ * Notifies the service that the app requested to remove content capture data.
+ *
+ * @param request the content capture data requested to be removed
+ */
+ public void onDataRemovalRequest(@NonNull DataRemovalRequest request) {
+ if (sVerbose) Log.v(TAG, "onDataRemovalRequest()");
+ }
+
+ /**
+ * Notifies the service that data has been shared via a readable file.
+ *
+ * @param request request object containing information about data being shared
+ * @param callback callback to be fired with response on whether the request is "needed" and can
+ * be handled by the Content Capture service.
+ *
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public void onDataShareRequest(@NonNull DataShareRequest request,
+ @NonNull DataShareCallback callback) {
+ if (sVerbose) Log.v(TAG, "onDataShareRequest()");
+ }
+
+ /**
+ * Notifies the service of {@link SnapshotData snapshot data} associated with an activity.
+ *
+ * @param sessionId the session's Id. This may also be
+ * {@link ContentCaptureSession#NO_SESSION_ID} if no content capture session
+ * exists for the activity being snapshotted
+ * @param snapshotData the data
+ */
+ public void onActivitySnapshot(@NonNull ContentCaptureSessionId sessionId,
+ @NonNull SnapshotData snapshotData) {
+ if (sVerbose) Log.v(TAG, "onActivitySnapshot(id=" + sessionId + ")");
+ }
+
+ /**
+ * Notifies the service of an activity-level event that is not associated with a session.
+ *
+ * <p>This method can be used to track some high-level events for all activities, even those
+ * that are not whitelisted for Content Capture.
+ *
+ * @param event high-level activity event
+ */
+ public void onActivityEvent(@NonNull ActivityEvent event) {
+ if (sVerbose) Log.v(TAG, "onActivityEvent(): " + event);
+ }
+
+ /**
+ * Destroys the content capture session.
+ *
+ * @param sessionId the id of the session to destroy
+ * */
+ public void onDestroyContentCaptureSession(@NonNull ContentCaptureSessionId sessionId) {
+ if (sVerbose) Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")");
+ }
+
+ /**
+ * Disables the Content Capture service for the given user.
+ */
+ public final void disableSelf() {
+ if (sDebug) Log.d(TAG, "disableSelf()");
+
+ final IContentCaptureServiceCallback callback = mCallback;
+ if (callback == null) {
+ Log.w(TAG, "disableSelf(): no server callback");
+ return;
+ }
+ try {
+ callback.disableSelf();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called when the Android system disconnects from the service.
+ *
+ * <p> At this point this service may no longer be an active {@link ContentCaptureService}.
+ * It should not make calls on {@link ContentCaptureManager} that requires the caller to be
+ * the current service.
+ */
+ public void onDisconnected() {
+ Slog.i(TAG, "unbinding from " + getClass().getName());
+ }
+
+ @Override
+ @CallSuper
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.print("Debug: "); pw.print(sDebug); pw.print(" Verbose: "); pw.println(sVerbose);
+ final int size = mSessionUids.size();
+ pw.print("Number sessions: "); pw.println(size);
+ if (size > 0) {
+ final String prefix = " ";
+ for (int i = 0; i < size; i++) {
+ pw.print(prefix); pw.print(mSessionUids.keyAt(i));
+ pw.print(": uid="); pw.println(mSessionUids.valueAt(i));
+ }
+ }
+ }
+
+ private void handleOnConnected(@NonNull IBinder callback) {
+ mCallback = IContentCaptureServiceCallback.Stub.asInterface(callback);
+ onConnected();
+ }
+
+ private void handleOnDisconnected() {
+ onDisconnected();
+ mCallback = null;
+ }
+
+ //TODO(b/111276913): consider caching the InteractionSessionId for the lifetime of the session,
+ // so we don't need to create a temporary InteractionSessionId for each event.
+
+ private void handleOnCreateSession(@NonNull ContentCaptureContext context,
+ int sessionId, int uid, IResultReceiver clientReceiver, int initialState) {
+ mSessionUids.put(sessionId, uid);
+ onCreateContentCaptureSession(context, new ContentCaptureSessionId(sessionId));
+
+ final int clientFlags = context.getFlags();
+ int stateFlags = 0;
+ if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE) != 0) {
+ stateFlags |= ContentCaptureSession.STATE_FLAG_SECURE;
+ }
+ if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_APP) != 0) {
+ stateFlags |= ContentCaptureSession.STATE_BY_APP;
+ }
+ if (stateFlags == 0) {
+ stateFlags = initialState;
+ } else {
+ stateFlags |= ContentCaptureSession.STATE_DISABLED;
+ }
+ setClientState(clientReceiver, stateFlags, mClientInterface.asBinder());
+ }
+
+ private void handleSendEvents(int uid,
+ @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents, int reason,
+ @Nullable ContentCaptureOptions options) {
+ final List<ContentCaptureEvent> events = parceledEvents.getList();
+ if (events.isEmpty()) {
+ Log.w(TAG, "handleSendEvents() received empty list of events");
+ return;
+ }
+
+ // Metrics.
+ final FlushMetrics metrics = new FlushMetrics();
+ ComponentName activityComponent = null;
+
+ // Most events belong to the same session, so we can keep a reference to the last one
+ // to avoid creating too many ContentCaptureSessionId objects
+ int lastSessionId = NO_SESSION_ID;
+ ContentCaptureSessionId sessionId = null;
+
+ for (int i = 0; i < events.size(); i++) {
+ final ContentCaptureEvent event = events.get(i);
+ if (!handleIsRightCallerFor(event, uid)) continue;
+ int sessionIdInt = event.getSessionId();
+ if (sessionIdInt != lastSessionId) {
+ sessionId = new ContentCaptureSessionId(sessionIdInt);
+ lastSessionId = sessionIdInt;
+ if (i != 0) {
+ writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason);
+ metrics.reset();
+ }
+ }
+ final ContentCaptureContext clientContext = event.getContentCaptureContext();
+ if (activityComponent == null && clientContext != null) {
+ activityComponent = clientContext.getActivityComponent();
+ }
+ switch (event.getType()) {
+ case ContentCaptureEvent.TYPE_SESSION_STARTED:
+ clientContext.setParentSessionId(event.getParentSessionId());
+ mSessionUids.put(sessionIdInt, uid);
+ onCreateContentCaptureSession(clientContext, sessionId);
+ metrics.sessionStarted++;
+ break;
+ case ContentCaptureEvent.TYPE_SESSION_FINISHED:
+ mSessionUids.delete(sessionIdInt);
+ onDestroyContentCaptureSession(sessionId);
+ metrics.sessionFinished++;
+ break;
+ case ContentCaptureEvent.TYPE_VIEW_APPEARED:
+ onContentCaptureEvent(sessionId, event);
+ metrics.viewAppearedCount++;
+ break;
+ case ContentCaptureEvent.TYPE_VIEW_DISAPPEARED:
+ onContentCaptureEvent(sessionId, event);
+ metrics.viewDisappearedCount++;
+ break;
+ case ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED:
+ onContentCaptureEvent(sessionId, event);
+ metrics.viewTextChangedCount++;
+ break;
+ default:
+ onContentCaptureEvent(sessionId, event);
+ }
+ }
+ writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason);
+ }
+
+ private void handleOnActivitySnapshot(int sessionId, @NonNull SnapshotData snapshotData) {
+ onActivitySnapshot(new ContentCaptureSessionId(sessionId), snapshotData);
+ }
+
+ private void handleFinishSession(int sessionId) {
+ mSessionUids.delete(sessionId);
+ onDestroyContentCaptureSession(new ContentCaptureSessionId(sessionId));
+ }
+
+ private void handleOnDataRemovalRequest(@NonNull DataRemovalRequest request) {
+ onDataRemovalRequest(request);
+ }
+
+ private void handleOnDataShared(@NonNull DataShareRequest request,
+ IDataShareCallback callback) {
+ onDataShareRequest(request, new DataShareCallback() {
+
+ @Override
+ public void onAccept(@NonNull Executor executor,
+ @NonNull DataShareReadAdapter adapter) {
+ Preconditions.checkNotNull(adapter);
+ Preconditions.checkNotNull(executor);
+
+ DataShareReadAdapterDelegate delegate =
+ new DataShareReadAdapterDelegate(executor, adapter,
+ mDataShareAdapterResourceManager);
+
+ try {
+ callback.accept(delegate);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to accept data sharing", e);
+ }
+ }
+
+ @Override
+ public void onReject() {
+ try {
+ callback.reject();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to reject data sharing", e);
+ }
+ }
+ });
+ }
+
+ private void handleOnActivityEvent(@NonNull ActivityEvent event) {
+ onActivityEvent(event);
+ }
+
+ /**
+ * Checks if the given {@code uid} owns the session associated with the event.
+ */
+ private boolean handleIsRightCallerFor(@NonNull ContentCaptureEvent event, int uid) {
+ final int sessionId;
+ switch (event.getType()) {
+ case ContentCaptureEvent.TYPE_SESSION_STARTED:
+ case ContentCaptureEvent.TYPE_SESSION_FINISHED:
+ sessionId = event.getParentSessionId();
+ break;
+ default:
+ sessionId = event.getSessionId();
+ }
+ if (mSessionUids.indexOfKey(sessionId) < 0) {
+ if (sVerbose) {
+ Log.v(TAG, "handleIsRightCallerFor(" + event + "): no session for " + sessionId
+ + ": " + mSessionUids);
+ }
+ // Just ignore, as the session could have been finished already
+ return false;
+ }
+ final int rightUid = mSessionUids.get(sessionId);
+ if (rightUid != uid) {
+ Log.e(TAG, "invalid call from UID " + uid + ": session " + sessionId + " belongs to "
+ + rightUid);
+ long now = System.currentTimeMillis();
+ if (now - mLastCallerMismatchLog > mCallerMismatchTimeout) {
+ FrameworkStatsLog.write(FrameworkStatsLog.CONTENT_CAPTURE_CALLER_MISMATCH_REPORTED,
+ getPackageManager().getNameForUid(rightUid),
+ getPackageManager().getNameForUid(uid));
+ mLastCallerMismatchLog = now;
+ }
+ return false;
+ }
+ return true;
+
+ }
+
+ /**
+ * Sends the state of the {@link ContentCaptureManager} in the client app.
+ *
+ * @param clientReceiver receiver in the client app.
+ * @param sessionState state of the session
+ * @param binder handle to the {@code IContentCaptureDirectManager} object that resides in the
+ * service.
+ * @hide
+ */
+ public static void setClientState(@NonNull IResultReceiver clientReceiver,
+ int sessionState, @Nullable IBinder binder) {
+ try {
+ final Bundle extras;
+ if (binder != null) {
+ extras = new Bundle();
+ extras.putBinder(MainContentCaptureSession.EXTRA_BINDER, binder);
+ } else {
+ extras = null;
+ }
+ clientReceiver.send(sessionState, extras);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Error async reporting result to client: " + e);
+ }
+ }
+
+ /**
+ * Logs the metrics for content capture events flushing.
+ */
+ private void writeFlushMetrics(int sessionId, @Nullable ComponentName app,
+ @NonNull FlushMetrics flushMetrics, @Nullable ContentCaptureOptions options,
+ int flushReason) {
+ if (mCallback == null) {
+ Log.w(TAG, "writeSessionFlush(): no server callback");
+ return;
+ }
+
+ try {
+ mCallback.writeSessionFlush(sessionId, app, flushMetrics, options, flushReason);
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to write flush metrics: " + e);
+ }
+ }
+
+ private static class DataShareReadAdapterDelegate extends IDataShareReadAdapter.Stub {
+
+ private final WeakReference<LocalDataShareAdapterResourceManager> mResourceManagerReference;
+ private final Object mLock = new Object();
+
+ DataShareReadAdapterDelegate(Executor executor, DataShareReadAdapter adapter,
+ LocalDataShareAdapterResourceManager resourceManager) {
+ Preconditions.checkNotNull(executor);
+ Preconditions.checkNotNull(adapter);
+ Preconditions.checkNotNull(resourceManager);
+
+ resourceManager.initializeForDelegate(this, adapter, executor);
+ mResourceManagerReference = new WeakReference<>(resourceManager);
+ }
+
+ @Override
+ public void start(ParcelFileDescriptor fd)
+ throws RemoteException {
+ synchronized (mLock) {
+ executeAdapterMethodLocked(adapter -> adapter.onStart(fd), "onStart");
+
+ // Client app and Service successfully connected, so this object would be kept alive
+ // until the session has finished.
+ clearHardReferences();
+ }
+ }
+
+ @Override
+ public void error(int errorCode) throws RemoteException {
+ synchronized (mLock) {
+ executeAdapterMethodLocked(
+ adapter -> adapter.onError(errorCode), "onError");
+ clearHardReferences();
+ }
+ }
+
+ private void executeAdapterMethodLocked(Consumer<DataShareReadAdapter> adapterFn,
+ String methodName) {
+ LocalDataShareAdapterResourceManager resourceManager = mResourceManagerReference.get();
+ if (resourceManager == null) {
+ Slog.w(TAG, "Can't execute " + methodName + "(), resource manager has been GC'ed");
+ return;
+ }
+
+ DataShareReadAdapter adapter = resourceManager.getAdapter(this);
+ Executor executor = resourceManager.getExecutor(this);
+
+ if (adapter == null || executor == null) {
+ Slog.w(TAG, "Can't execute " + methodName + "(), references are null");
+ return;
+ }
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() -> adapterFn.accept(adapter));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ private void clearHardReferences() {
+ LocalDataShareAdapterResourceManager resourceManager = mResourceManagerReference.get();
+ if (resourceManager == null) {
+ Slog.w(TAG, "Can't clear references, resource manager has been GC'ed");
+ return;
+ }
+
+ resourceManager.clearHardReferences(this);
+ }
+ }
+
+ /**
+ * Wrapper class making sure dependencies on the current application stay in the application
+ * context.
+ */
+ private static class LocalDataShareAdapterResourceManager {
+
+ // Keeping hard references to the remote objects in the current process (static context)
+ // to prevent them to be gc'ed during the lifetime of the application. This is an
+ // artifact of only operating with weak references remotely: there has to be at least 1
+ // hard reference in order for this to not be killed.
+ private Map<DataShareReadAdapterDelegate, DataShareReadAdapter>
+ mDataShareReadAdapterHardReferences = new HashMap<>();
+ private Map<DataShareReadAdapterDelegate, Executor> mExecutorHardReferences =
+ new HashMap<>();
+
+
+ void initializeForDelegate(DataShareReadAdapterDelegate delegate,
+ DataShareReadAdapter adapter, Executor executor) {
+ mDataShareReadAdapterHardReferences.put(delegate, adapter);
+ mExecutorHardReferences.put(delegate, executor);
+ }
+
+ Executor getExecutor(DataShareReadAdapterDelegate delegate) {
+ return mExecutorHardReferences.get(delegate);
+ }
+
+ DataShareReadAdapter getAdapter(DataShareReadAdapterDelegate delegate) {
+ return mDataShareReadAdapterHardReferences.get(delegate);
+ }
+
+ void clearHardReferences(DataShareReadAdapterDelegate delegate) {
+ mDataShareReadAdapterHardReferences.remove(delegate);
+ mExecutorHardReferences.remove(delegate);
+ }
+ }
+}
diff --git a/android/service/contentcapture/ContentCaptureServiceInfo.java b/android/service/contentcapture/ContentCaptureServiceInfo.java
new file mode 100644
index 0000000..fb60619
--- /dev/null
+++ b/android/service/contentcapture/ContentCaptureServiceInfo.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2019 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.contentcapture;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * {@link ServiceInfo} and meta-data about an {@link ContentCaptureService}.
+ *
+ * @hide
+ */
+public final class ContentCaptureServiceInfo {
+
+ private static final String TAG = ContentCaptureServiceInfo.class.getSimpleName();
+ private static final String XML_TAG_SERVICE = "content-capture-service";
+
+ private static ServiceInfo getServiceInfoOrThrow(ComponentName comp, boolean isTemp,
+ @UserIdInt int userId) throws PackageManager.NameNotFoundException {
+ int flags = PackageManager.GET_META_DATA;
+ if (!isTemp) {
+ flags |= PackageManager.MATCH_SYSTEM_ONLY;
+ }
+
+ ServiceInfo si = null;
+ try {
+ si = AppGlobals.getPackageManager().getServiceInfo(comp, flags, userId);
+ } catch (RemoteException e) {
+ }
+ if (si == null) {
+ throw new NameNotFoundException("Could not get serviceInfo for "
+ + (isTemp ? " (temp)" : "(default system)")
+ + " " + comp.flattenToShortString());
+ }
+ return si;
+ }
+
+ @NonNull
+ private final ServiceInfo mServiceInfo;
+
+ @Nullable
+ private final String mSettingsActivity;
+
+ public ContentCaptureServiceInfo(@NonNull Context context, @NonNull ComponentName comp,
+ boolean isTemporaryService, @UserIdInt int userId)
+ throws PackageManager.NameNotFoundException {
+ this(context, getServiceInfoOrThrow(comp, isTemporaryService, userId));
+ }
+
+ private ContentCaptureServiceInfo(@NonNull Context context, @NonNull ServiceInfo si) {
+ // Check for permissions.
+ if (!Manifest.permission.BIND_CONTENT_CAPTURE_SERVICE.equals(si.permission)) {
+ Slog.w(TAG, "ContentCaptureService from '" + si.packageName
+ + "' does not require permission "
+ + Manifest.permission.BIND_CONTENT_CAPTURE_SERVICE);
+ throw new SecurityException("Service does not require permission "
+ + Manifest.permission.BIND_CONTENT_CAPTURE_SERVICE);
+ }
+
+ mServiceInfo = si;
+
+ // Get the metadata, if declared.
+ final XmlResourceParser parser = si.loadXmlMetaData(context.getPackageManager(),
+ ContentCaptureService.SERVICE_META_DATA);
+ if (parser == null) {
+ mSettingsActivity = null;
+ return;
+ }
+
+ String settingsActivity = null;
+
+ try {
+ final Resources resources = context.getPackageManager().getResourcesForApplication(
+ si.applicationInfo);
+
+ int type = 0;
+ while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
+ type = parser.next();
+ }
+
+ if (XML_TAG_SERVICE.equals(parser.getName())) {
+ final AttributeSet allAttributes = Xml.asAttributeSet(parser);
+ TypedArray afsAttributes = null;
+ try {
+ afsAttributes = resources.obtainAttributes(allAttributes,
+ com.android.internal.R.styleable.ContentCaptureService);
+ settingsActivity = afsAttributes.getString(
+ R.styleable.ContentCaptureService_settingsActivity);
+ } finally {
+ if (afsAttributes != null) {
+ afsAttributes.recycle();
+ }
+ }
+ } else {
+ Log.e(TAG, "Meta-data does not start with content-capture-service tag");
+ }
+ } catch (PackageManager.NameNotFoundException | IOException | XmlPullParserException e) {
+ Log.e(TAG, "Error parsing auto fill service meta-data", e);
+ }
+
+ mSettingsActivity = settingsActivity;
+ }
+
+ @NonNull
+ public ServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ @Nullable
+ public String getSettingsActivity() {
+ return mSettingsActivity;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(getClass().getSimpleName());
+ builder.append("[").append(mServiceInfo);
+ builder.append(", settings:").append(mSettingsActivity);
+ return builder.toString();
+ }
+
+ /**
+ * Dumps it!
+ */
+ public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
+ pw.print(prefix);
+ pw.print("Component: ");
+ pw.println(getServiceInfo().getComponentName());
+ pw.print(prefix);
+ pw.print("Settings: ");
+ pw.println(mSettingsActivity);
+ }
+}
diff --git a/android/service/contentcapture/DataShareCallback.java b/android/service/contentcapture/DataShareCallback.java
new file mode 100644
index 0000000..5df8a4b
--- /dev/null
+++ b/android/service/contentcapture/DataShareCallback.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 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.contentcapture;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Callback for the Content Capture Service to accept or reject the data share request from a client
+ * app.
+ *
+ * If the request is rejected, client app would receive a signal and the data share session wouldn't
+ * be started.
+ *
+ * @hide
+ **/
+@SystemApi
+@TestApi
+public interface DataShareCallback {
+
+ /** Accept the data share.
+ *
+ * @param executor executor to be used for running the adapter in.
+ * @param adapter adapter to be used for the share operation
+ */
+ void onAccept(@NonNull @CallbackExecutor Executor executor,
+ @NonNull DataShareReadAdapter adapter);
+
+ /** Reject the data share. */
+ void onReject();
+}
diff --git a/android/service/contentcapture/DataShareReadAdapter.java b/android/service/contentcapture/DataShareReadAdapter.java
new file mode 100644
index 0000000..8cd9eea
--- /dev/null
+++ b/android/service/contentcapture/DataShareReadAdapter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 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.contentcapture;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.ParcelFileDescriptor;
+import android.view.contentcapture.ContentCaptureManager.DataShareError;
+
+/**
+ * Adapter class to be used for the Content Capture Service app to propagate the status of the
+ * session
+ *
+ * @hide
+ **/
+@SystemApi
+@TestApi
+public interface DataShareReadAdapter {
+
+ /**
+ * Signals the start of the data sharing session.
+ *
+ * @param fd file descriptor to use for reading data, that's being shared
+ **/
+ void onStart(@NonNull ParcelFileDescriptor fd);
+
+ /**
+ * Signals that the session failed to start or terminated unsuccessfully.
+ *
+ * <p>Important: together with the error, file sharing stream might be closed, and therefore
+ * reading from {@code fd} from {@link #onStart} will result in the end of stream. The order of
+ * these 2 events is not defined, and it's important that the service treats end of stream
+ * correctly in this situation.
+ **/
+ void onError(@DataShareError int errorCode);
+}
diff --git a/android/service/contentcapture/FlushMetrics.java b/android/service/contentcapture/FlushMetrics.java
new file mode 100644
index 0000000..01f3a12
--- /dev/null
+++ b/android/service/contentcapture/FlushMetrics.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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.contentcapture;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Holds metrics for content capture events flushing.
+ *
+ * @hide
+ */
+public final class FlushMetrics implements Parcelable {
+ public int viewAppearedCount;
+ public int viewDisappearedCount;
+ public int viewTextChangedCount;
+ public int sessionStarted;
+ public int sessionFinished;
+
+ /**
+ * Resets all flush metrics.
+ */
+ public void reset() {
+ viewAppearedCount = 0;
+ viewDisappearedCount = 0;
+ viewTextChangedCount = 0;
+ sessionStarted = 0;
+ sessionFinished = 0;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(sessionStarted);
+ out.writeInt(sessionFinished);
+ out.writeInt(viewAppearedCount);
+ out.writeInt(viewDisappearedCount);
+ out.writeInt(viewTextChangedCount);
+ }
+
+ @NonNull
+ public static final Creator<FlushMetrics> CREATOR = new Creator<FlushMetrics>() {
+ @NonNull
+ @Override
+ public FlushMetrics createFromParcel(Parcel in) {
+ final FlushMetrics flushMetrics = new FlushMetrics();
+ flushMetrics.sessionStarted = in.readInt();
+ flushMetrics.sessionFinished = in.readInt();
+ flushMetrics.viewAppearedCount = in.readInt();
+ flushMetrics.viewDisappearedCount = in.readInt();
+ flushMetrics.viewTextChangedCount = in.readInt();
+ return flushMetrics;
+ }
+
+ @Override
+ public FlushMetrics[] newArray(int size) {
+ return new FlushMetrics[size];
+ }
+ };
+}
diff --git a/android/service/contentcapture/SnapshotData.java b/android/service/contentcapture/SnapshotData.java
new file mode 100644
index 0000000..5b3930a
--- /dev/null
+++ b/android/service/contentcapture/SnapshotData.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 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.contentcapture;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.assist.AssistContent;
+import android.app.assist.AssistStructure;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A container class for data taken from a snapshot of an activity.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class SnapshotData implements Parcelable {
+
+ private final @NonNull Bundle mAssistData;
+ private final @NonNull AssistStructure mAssistStructure;
+ private final @Nullable AssistContent mAssistContent;
+
+ /**
+ * Creates a new instance.
+ *
+ * @hide
+ */
+ public SnapshotData(@NonNull Bundle assistData, @NonNull AssistStructure assistStructure,
+ @Nullable AssistContent assistContent) {
+ mAssistData = assistData;
+ mAssistStructure = assistStructure;
+ mAssistContent = assistContent;
+ }
+
+ SnapshotData(@NonNull Parcel parcel) {
+ mAssistData = parcel.readBundle();
+ mAssistStructure = parcel.readParcelable(null);
+ mAssistContent = parcel.readParcelable(null);
+ }
+
+ /**
+ * Returns the assist data for this snapshot.
+ */
+ @NonNull
+ public Bundle getAssistData() {
+ return mAssistData;
+ }
+
+ /**
+ * Returns the assist structure for this snapshot.
+ */
+ @NonNull
+ public AssistStructure getAssistStructure() {
+ return mAssistStructure;
+ }
+
+ /**
+ * Returns the assist context for this snapshot.
+ */
+ @Nullable
+ public AssistContent getAssistContent() {
+ return mAssistContent;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel parcel, int flags) {
+ parcel.writeBundle(mAssistData);
+ parcel.writeParcelable(mAssistStructure, flags);
+ parcel.writeParcelable(mAssistContent, flags);
+ }
+
+ public static final @android.annotation.NonNull Creator<SnapshotData> CREATOR =
+ new Creator<SnapshotData>() {
+
+ @Override
+ @NonNull
+ public SnapshotData createFromParcel(@NonNull Parcel parcel) {
+ return new SnapshotData(parcel);
+ }
+
+ @Override
+ @NonNull
+ public SnapshotData[] newArray(int size) {
+ return new SnapshotData[size];
+ }
+ };
+}
diff --git a/android/service/contentsuggestions/ContentSuggestionsService.java b/android/service/contentsuggestions/ContentSuggestionsService.java
new file mode 100644
index 0000000..306b483
--- /dev/null
+++ b/android/service/contentsuggestions/ContentSuggestionsService.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2018 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.contentsuggestions;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.app.contentsuggestions.ClassificationsRequest;
+import android.app.contentsuggestions.ContentSuggestionsManager;
+import android.app.contentsuggestions.IClassificationsCallback;
+import android.app.contentsuggestions.ISelectionsCallback;
+import android.app.contentsuggestions.SelectionsRequest;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.GraphicBuffer;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+
+/**
+ * @hide
+ */
+@SystemApi
+public abstract class ContentSuggestionsService extends Service {
+
+ private static final String TAG = ContentSuggestionsService.class.getSimpleName();
+
+ private Handler mHandler;
+
+ /**
+ * The action for the intent used to define the content suggestions service.
+ *
+ * <p>To be supported, the service must also require the
+ * * {@link android.Manifest.permission#BIND_CONTENT_SUGGESTIONS_SERVICE} permission so
+ * * that other applications can not abuse it.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.contentsuggestions.ContentSuggestionsService";
+
+ private final IContentSuggestionsService mInterface = new IContentSuggestionsService.Stub() {
+ @Override
+ public void provideContextImage(int taskId, GraphicBuffer contextImage,
+ int colorSpaceId, Bundle imageContextRequestExtras) {
+ if (imageContextRequestExtras.containsKey(ContentSuggestionsManager.EXTRA_BITMAP)
+ && contextImage != null) {
+ throw new IllegalArgumentException("Two bitmaps provided; expected one.");
+ }
+
+ Bitmap wrappedBuffer = null;
+ if (imageContextRequestExtras.containsKey(ContentSuggestionsManager.EXTRA_BITMAP)) {
+ wrappedBuffer = imageContextRequestExtras.getParcelable(
+ ContentSuggestionsManager.EXTRA_BITMAP);
+ } else {
+ if (contextImage != null) {
+ ColorSpace colorSpace = null;
+ if (colorSpaceId >= 0 && colorSpaceId < ColorSpace.Named.values().length) {
+ colorSpace = ColorSpace.get(ColorSpace.Named.values()[colorSpaceId]);
+ }
+ wrappedBuffer = Bitmap.wrapHardwareBuffer(contextImage, colorSpace);
+ }
+ }
+
+ mHandler.sendMessage(
+ obtainMessage(ContentSuggestionsService::onProcessContextImage,
+ ContentSuggestionsService.this, taskId,
+ wrappedBuffer,
+ imageContextRequestExtras));
+ }
+
+ @Override
+ public void suggestContentSelections(SelectionsRequest request,
+ ISelectionsCallback callback) {
+ mHandler.sendMessage(obtainMessage(
+ ContentSuggestionsService::onSuggestContentSelections,
+ ContentSuggestionsService.this, request, wrapSelectionsCallback(callback)));
+
+ }
+
+ @Override
+ public void classifyContentSelections(ClassificationsRequest request,
+ IClassificationsCallback callback) {
+ mHandler.sendMessage(obtainMessage(
+ ContentSuggestionsService::onClassifyContentSelections,
+ ContentSuggestionsService.this, request, wrapClassificationCallback(callback)));
+ }
+
+ @Override
+ public void notifyInteraction(String requestId, Bundle interaction) {
+ mHandler.sendMessage(
+ obtainMessage(ContentSuggestionsService::onNotifyInteraction,
+ ContentSuggestionsService.this, requestId, interaction));
+ }
+ };
+
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler(Looper.getMainLooper(), null, true);
+ }
+
+ /** @hide */
+ @Override
+ public final IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mInterface.asBinder();
+ }
+ Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
+ return null;
+ }
+
+ /**
+ * Called by the system to provide the snapshot for the task associated with the given
+ * {@param taskId}.
+ */
+ public abstract void onProcessContextImage(
+ int taskId, @Nullable Bitmap contextImage, @NonNull Bundle extras);
+
+ /**
+ * Content selections have been request through {@link ContentSuggestionsManager}, implementer
+ * should reply on the callback with selections.
+ */
+ public abstract void onSuggestContentSelections(@NonNull SelectionsRequest request,
+ @NonNull ContentSuggestionsManager.SelectionsCallback callback);
+
+ /**
+ * Content classifications have been request through {@link ContentSuggestionsManager},
+ * implementer should reply on the callback with classifications.
+ */
+ public abstract void onClassifyContentSelections(@NonNull ClassificationsRequest request,
+ @NonNull ContentSuggestionsManager.ClassificationsCallback callback);
+
+ /**
+ * User interactions have been reported through {@link ContentSuggestionsManager}, implementer
+ * should handle those interactions.
+ */
+ public abstract void onNotifyInteraction(
+ @NonNull String requestId, @NonNull Bundle interaction);
+
+ private ContentSuggestionsManager.SelectionsCallback wrapSelectionsCallback(
+ ISelectionsCallback callback) {
+ return (statusCode, selections) -> {
+ try {
+ callback.onContentSelectionsAvailable(statusCode, selections);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error sending result: " + e);
+ }
+ };
+ }
+
+ private ContentSuggestionsManager.ClassificationsCallback wrapClassificationCallback(
+ IClassificationsCallback callback) {
+ return ((statusCode, classifications) -> {
+ try {
+ callback.onContentClassificationsAvailable(statusCode, classifications);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error sending result: " + e);
+ }
+ });
+ }
+}
diff --git a/android/service/controls/Control.java b/android/service/controls/Control.java
new file mode 100644
index 0000000..d01bc25
--- /dev/null
+++ b/android/service/controls/Control.java
@@ -0,0 +1,823 @@
+/*
+ * Copyright (C) 2019 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.controls;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.controls.actions.ControlAction;
+import android.service.controls.templates.ControlTemplate;
+import android.service.controls.templates.ControlTemplateWrapper;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents a physical object that can be represented by a {@link ControlTemplate} and whose
+ * properties may be modified through a {@link ControlAction}.
+ *
+ * The information is provided by a {@link ControlsProviderService} and represents static
+ * information (not current status) about the device.
+ * <p>
+ * Each control needs a unique (per provider) identifier that is persistent across reboots of the
+ * system.
+ * <p>
+ * Each {@link Control} will have a name, a subtitle and will optionally belong to a structure
+ * and zone. Some of these values are defined by the user and/or the {@link ControlsProviderService}
+ * and will be used to display the control as well as group them for management.
+ * <p>
+ * Each object will have an associated {@link DeviceTypes.DeviceType}. This will determine the icons and colors
+ * used to display it.
+ * <p>
+ * An {@link Intent} linking to the provider Activity that expands on this {@link Control} and
+ * allows for further actions should be provided.
+ */
+public final class Control implements Parcelable {
+ private static final String TAG = "Control";
+
+ private static final int NUM_STATUS = 5;
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATUS_UNKNOWN,
+ STATUS_OK,
+ STATUS_NOT_FOUND,
+ STATUS_ERROR,
+ STATUS_DISABLED,
+ })
+ public @interface Status {};
+
+ public static final int STATUS_UNKNOWN = 0;
+
+ /**
+ * The device corresponding to the {@link Control} is responding correctly.
+ */
+ public static final int STATUS_OK = 1;
+
+ /**
+ * The device corresponding to the {@link Control} cannot be found or was removed.
+ */
+ public static final int STATUS_NOT_FOUND = 2;
+
+ /**
+ * The device corresponding to the {@link Control} is in an error state.
+ */
+ public static final int STATUS_ERROR = 3;
+
+ /**
+ * The {@link Control} is currently disabled.
+ */
+ public static final int STATUS_DISABLED = 4;
+
+ private final @NonNull String mControlId;
+ private final @DeviceTypes.DeviceType int mDeviceType;
+ private final @NonNull CharSequence mTitle;
+ private final @NonNull CharSequence mSubtitle;
+ private final @Nullable CharSequence mStructure;
+ private final @Nullable CharSequence mZone;
+ private final @NonNull PendingIntent mAppIntent;
+
+ private final @Nullable Icon mCustomIcon;
+ private final @Nullable ColorStateList mCustomColor;
+
+ private final @Status int mStatus;
+ private final @NonNull ControlTemplate mControlTemplate;
+ private final @NonNull CharSequence mStatusText;
+
+ /**
+ * @param controlId the unique persistent identifier for this object.
+ * @param deviceType the type of device for this control. This will determine icons and colors.
+ * @param title the user facing name of this control (e.g. "Bedroom thermostat").
+ * @param subtitle a user facing subtitle with extra information about this control
+ * @param structure a user facing name for the structure containing the device associated with
+ * this control.
+ * @param zone
+ * @param appIntent a {@link PendingIntent} linking to a page to interact with the
+ * corresponding device.
+ * @param customIcon
+ * @param customColor
+ * @param status
+ * @param controlTemplate
+ * @param statusText
+ */
+ Control(@NonNull String controlId,
+ @DeviceTypes.DeviceType int deviceType,
+ @NonNull CharSequence title,
+ @NonNull CharSequence subtitle,
+ @Nullable CharSequence structure,
+ @Nullable CharSequence zone,
+ @NonNull PendingIntent appIntent,
+ @Nullable Icon customIcon,
+ @Nullable ColorStateList customColor,
+ @Status int status,
+ @NonNull ControlTemplate controlTemplate,
+ @NonNull CharSequence statusText) {
+ Preconditions.checkNotNull(controlId);
+ Preconditions.checkNotNull(title);
+ Preconditions.checkNotNull(subtitle);
+ Preconditions.checkNotNull(appIntent);
+ Preconditions.checkNotNull(controlTemplate);
+ Preconditions.checkNotNull(statusText);
+ mControlId = controlId;
+ if (!DeviceTypes.validDeviceType(deviceType)) {
+ Log.e(TAG, "Invalid device type:" + deviceType);
+ mDeviceType = DeviceTypes.TYPE_UNKNOWN;
+ } else {
+ mDeviceType = deviceType;
+ }
+ mTitle = title;
+ mSubtitle = subtitle;
+ mStructure = structure;
+ mZone = zone;
+ mAppIntent = appIntent;
+
+ mCustomColor = customColor;
+ mCustomIcon = customIcon;
+
+ if (status < 0 || status >= NUM_STATUS) {
+ mStatus = STATUS_UNKNOWN;
+ Log.e(TAG, "Status unknown:" + status);
+ } else {
+ mStatus = status;
+ }
+ mControlTemplate = controlTemplate;
+ mStatusText = statusText;
+ }
+
+ /**
+ * @param in
+ * @hide
+ */
+ Control(Parcel in) {
+ mControlId = in.readString();
+ mDeviceType = in.readInt();
+ mTitle = in.readCharSequence();
+ mSubtitle = in.readCharSequence();
+ if (in.readByte() == (byte) 1) {
+ mStructure = in.readCharSequence();
+ } else {
+ mStructure = null;
+ }
+ if (in.readByte() == (byte) 1) {
+ mZone = in.readCharSequence();
+ } else {
+ mZone = null;
+ }
+ mAppIntent = PendingIntent.CREATOR.createFromParcel(in);
+
+ if (in.readByte() == (byte) 1) {
+ mCustomIcon = Icon.CREATOR.createFromParcel(in);
+ } else {
+ mCustomIcon = null;
+ }
+
+ if (in.readByte() == (byte) 1) {
+ mCustomColor = ColorStateList.CREATOR.createFromParcel(in);
+ } else {
+ mCustomColor = null;
+ }
+
+ mStatus = in.readInt();
+ ControlTemplateWrapper wrapper = ControlTemplateWrapper.CREATOR.createFromParcel(in);
+ mControlTemplate = wrapper.getWrappedTemplate();
+ mStatusText = in.readCharSequence();
+ }
+
+ /**
+ * @return the identifier for the {@link Control}
+ */
+ @NonNull
+ public String getControlId() {
+ return mControlId;
+ }
+
+
+ /**
+ * @return type of device represented by this {@link Control}, used to determine the default
+ * icon and color
+ */
+ @DeviceTypes.DeviceType
+ public int getDeviceType() {
+ return mDeviceType;
+ }
+
+ /**
+ * @return the user facing name of the {@link Control}
+ */
+ @NonNull
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * @return additional information about the {@link Control}, to appear underneath the title
+ */
+ @NonNull
+ public CharSequence getSubtitle() {
+ return mSubtitle;
+ }
+
+ /**
+ * Optional top-level group to help define the {@link Control}'s location, visible to the user.
+ * If not present, the application name will be used as the top-level group. A structure
+ * contains zones which contains controls.
+ *
+ * @return name of the structure containing the control
+ */
+ @Nullable
+ public CharSequence getStructure() {
+ return mStructure;
+ }
+
+ /**
+ * Optional group name to help define the {@link Control}'s location within a structure,
+ * visible to the user. A structure contains zones which contains controls.
+ *
+ * @return name of the zone containing the control
+ */
+ @Nullable
+ public CharSequence getZone() {
+ return mZone;
+ }
+
+ /**
+ * @return a {@link PendingIntent} linking to an Activity for the {@link Control}
+ */
+ @NonNull
+ public PendingIntent getAppIntent() {
+ return mAppIntent;
+ }
+
+ /**
+ * Optional icon to be shown with the {@link Control}. It is highly recommended
+ * to let the system default the icon unless the default icon is not suitable.
+ *
+ * @return icon to show
+ */
+ @Nullable
+ public Icon getCustomIcon() {
+ return mCustomIcon;
+ }
+
+ /**
+ * Optional color to be shown with the {@link Control}. It is highly recommended
+ * to let the system default the color unless the default is not suitable for the
+ * application.
+ *
+ * @return background color to use
+ */
+ @Nullable
+ public ColorStateList getCustomColor() {
+ return mCustomColor;
+ }
+
+ /**
+ * @return status of the {@link Control}, used to convey information about the attempt to
+ * fetch the current state
+ */
+ @Status
+ public int getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * @return instance of {@link ControlTemplate}, that defines how the {@link Control} will
+ * behave and what interactions are available to the user
+ */
+ @NonNull
+ public ControlTemplate getControlTemplate() {
+ return mControlTemplate;
+ }
+
+ /**
+ * @return user-facing text description of the {@link Control}'s status, describing its current
+ * state
+ */
+ @NonNull
+ public CharSequence getStatusText() {
+ return mStatusText;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mControlId);
+ dest.writeInt(mDeviceType);
+ dest.writeCharSequence(mTitle);
+ dest.writeCharSequence(mSubtitle);
+ if (mStructure != null) {
+ dest.writeByte((byte) 1);
+ dest.writeCharSequence(mStructure);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ if (mZone != null) {
+ dest.writeByte((byte) 1);
+ dest.writeCharSequence(mZone);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ mAppIntent.writeToParcel(dest, flags);
+ if (mCustomIcon != null) {
+ dest.writeByte((byte) 1);
+ mCustomIcon.writeToParcel(dest, flags);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ if (mCustomColor != null) {
+ dest.writeByte((byte) 1);
+ mCustomColor.writeToParcel(dest, flags);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+
+ dest.writeInt(mStatus);
+ new ControlTemplateWrapper(mControlTemplate).writeToParcel(dest, flags);
+ dest.writeCharSequence(mStatusText);
+ }
+
+ public static final @NonNull Creator<Control> CREATOR = new Creator<Control>() {
+ @Override
+ public Control createFromParcel(@NonNull Parcel source) {
+ return new Control(source);
+ }
+
+ @Override
+ public Control[] newArray(int size) {
+ return new Control[size];
+ }
+ };
+
+ /**
+ * Builder class for {@link Control}.
+ *
+ * This class facilitates the creation of {@link Control} with no state. Must be used to
+ * provide controls for {@link ControlsProviderService#createPublisherForAllAvailable} and
+ * {@link ControlsProviderService#createPublisherForSuggested}.
+ *
+ * It provides the following defaults for non-optional parameters:
+ * <ul>
+ * <li> Device type: {@link DeviceTypes#TYPE_UNKNOWN}
+ * <li> Title: {@code ""}
+ * <li> Subtitle: {@code ""}
+ * </ul>
+ * This fixes the values relating to state of the {@link Control} as required by
+ * {@link ControlsProviderService#createPublisherForAllAvailable}:
+ * <ul>
+ * <li> Status: {@link Status#STATUS_UNKNOWN}
+ * <li> Control template: {@link ControlTemplate#getNoTemplateObject}
+ * <li> Status text: {@code ""}
+ * </ul>
+ */
+ @SuppressLint("MutableBareField")
+ public static final class StatelessBuilder {
+ private static final String TAG = "StatelessBuilder";
+ private @NonNull String mControlId;
+ private @DeviceTypes.DeviceType int mDeviceType = DeviceTypes.TYPE_UNKNOWN;
+ private @NonNull CharSequence mTitle = "";
+ private @NonNull CharSequence mSubtitle = "";
+ private @Nullable CharSequence mStructure;
+ private @Nullable CharSequence mZone;
+ private @NonNull PendingIntent mAppIntent;
+ private @Nullable Icon mCustomIcon;
+ private @Nullable ColorStateList mCustomColor;
+
+ /**
+ * @param controlId the identifier for the {@link Control}
+ * @param appIntent the pending intent linking to the device Activity
+ */
+ public StatelessBuilder(@NonNull String controlId,
+ @NonNull PendingIntent appIntent) {
+ Preconditions.checkNotNull(controlId);
+ Preconditions.checkNotNull(appIntent);
+ mControlId = controlId;
+ mAppIntent = appIntent;
+ }
+
+ /**
+ * Creates a {@link StatelessBuilder} using an existing {@link Control} as a base.
+ *
+ * @param control base for the builder.
+ */
+ public StatelessBuilder(@NonNull Control control) {
+ Preconditions.checkNotNull(control);
+ mControlId = control.mControlId;
+ mDeviceType = control.mDeviceType;
+ mTitle = control.mTitle;
+ mSubtitle = control.mSubtitle;
+ mStructure = control.mStructure;
+ mZone = control.mZone;
+ mAppIntent = control.mAppIntent;
+ mCustomIcon = control.mCustomIcon;
+ mCustomColor = control.mCustomColor;
+ }
+
+ /**
+ * @param controlId the identifier for the {@link Control}
+ * @return {@code this}
+ */
+ @NonNull
+ public StatelessBuilder setControlId(@NonNull String controlId) {
+ Preconditions.checkNotNull(controlId);
+ mControlId = controlId;
+ return this;
+ }
+
+ /**
+ * @param deviceType type of device represented by this {@link Control}, used to
+ * determine the default icon and color
+ * @return {@code this}
+ */
+ @NonNull
+ public StatelessBuilder setDeviceType(@DeviceTypes.DeviceType int deviceType) {
+ if (!DeviceTypes.validDeviceType(deviceType)) {
+ Log.e(TAG, "Invalid device type:" + deviceType);
+ mDeviceType = DeviceTypes.TYPE_UNKNOWN;
+ } else {
+ mDeviceType = deviceType;
+ }
+ return this;
+ }
+
+ /**
+ * @param title the user facing name of the {@link Control}
+ * @return {@code this}
+ */
+ @NonNull
+ public StatelessBuilder setTitle(@NonNull CharSequence title) {
+ Preconditions.checkNotNull(title);
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * @param subtitle additional information about the {@link Control}, to appear underneath
+ * the title
+ * @return {@code this}
+ */
+ @NonNull
+ public StatelessBuilder setSubtitle(@NonNull CharSequence subtitle) {
+ Preconditions.checkNotNull(subtitle);
+ mSubtitle = subtitle;
+ return this;
+ }
+
+ /**
+ * Optional top-level group to help define the {@link Control}'s location, visible to the
+ * user. If not present, the application name will be used as the top-level group. A
+ * structure contains zones which contains controls.
+ *
+ * @param structure name of the structure containing the control
+ * @return {@code this}
+ */
+ @NonNull
+ public StatelessBuilder setStructure(@Nullable CharSequence structure) {
+ mStructure = structure;
+ return this;
+ }
+
+ /**
+ * Optional group name to help define the {@link Control}'s location within a structure,
+ * visible to the user. A structure contains zones which contains controls.
+ *
+ * @param zone name of the zone containing the control
+ * @return {@code this}
+ */
+ @NonNull
+ public StatelessBuilder setZone(@Nullable CharSequence zone) {
+ mZone = zone;
+ return this;
+ }
+
+ /**
+ * @param appIntent a {@link PendingIntent} linking to an Activity for the {@link Control}
+ * @return {@code this}
+ */
+ @NonNull
+ public StatelessBuilder setAppIntent(@NonNull PendingIntent appIntent) {
+ Preconditions.checkNotNull(appIntent);
+ mAppIntent = appIntent;
+ return this;
+ }
+
+ /**
+ * Optional icon to be shown with the {@link Control}. It is highly recommended
+ * to let the system default the icon unless the default icon is not suitable.
+ *
+ * @param customIcon icon to show
+ * @return {@code this}
+ */
+ @NonNull
+ public StatelessBuilder setCustomIcon(@Nullable Icon customIcon) {
+ mCustomIcon = customIcon;
+ return this;
+ }
+
+ /**
+ * Optional color to be shown with the {@link Control}. It is highly recommended
+ * to let the system default the color unless the default is not suitable for the
+ * application.
+ *
+ * @param customColor background color to use
+ * @return {@code this}
+ */
+ @NonNull
+ public StatelessBuilder setCustomColor(@Nullable ColorStateList customColor) {
+ mCustomColor = customColor;
+ return this;
+ }
+
+ /**
+ * @return a valid {@link Control}
+ */
+ @NonNull
+ public Control build() {
+ return new Control(mControlId,
+ mDeviceType,
+ mTitle,
+ mSubtitle,
+ mStructure,
+ mZone,
+ mAppIntent,
+ mCustomIcon,
+ mCustomColor,
+ STATUS_UNKNOWN,
+ ControlTemplate.NO_TEMPLATE,
+ "");
+ }
+ }
+
+ /**
+ * Builder class for {@link Control} that contains state information.
+ *
+ * State information is passed through an instance of a {@link ControlTemplate} and will
+ * determine how the user can interact with the {@link Control}. User interactions will
+ * be sent through the method call {@link ControlsProviderService#performControlAction}
+ * with an instance of {@link ControlAction} to convey any potential new value.
+ *
+ * Must be used to provide controls for {@link ControlsProviderService#createPublisherFor}.
+ *
+ * It provides the following defaults for non-optional parameters:
+ * <ul>
+ * <li> Device type: {@link DeviceTypes#TYPE_UNKNOWN}
+ * <li> Title: {@code ""}
+ * <li> Subtitle: {@code ""}
+ * <li> Status: {@link Status#STATUS_UNKNOWN}
+ * <li> Control template: {@link ControlTemplate#getNoTemplateObject}
+ * <li> Status text: {@code ""}
+ * </ul>
+ */
+ public static final class StatefulBuilder {
+ private static final String TAG = "StatefulBuilder";
+ private @NonNull String mControlId;
+ private @DeviceTypes.DeviceType int mDeviceType = DeviceTypes.TYPE_UNKNOWN;
+ private @NonNull CharSequence mTitle = "";
+ private @NonNull CharSequence mSubtitle = "";
+ private @Nullable CharSequence mStructure;
+ private @Nullable CharSequence mZone;
+ private @NonNull PendingIntent mAppIntent;
+ private @Nullable Icon mCustomIcon;
+ private @Nullable ColorStateList mCustomColor;
+ private @Status int mStatus = STATUS_UNKNOWN;
+ private @NonNull ControlTemplate mControlTemplate = ControlTemplate.NO_TEMPLATE;
+ private @NonNull CharSequence mStatusText = "";
+
+ /**
+ * @param controlId the identifier for the {@link Control}.
+ * @param appIntent the pending intent linking to the device Activity.
+ */
+ public StatefulBuilder(@NonNull String controlId,
+ @NonNull PendingIntent appIntent) {
+ Preconditions.checkNotNull(controlId);
+ Preconditions.checkNotNull(appIntent);
+ mControlId = controlId;
+ mAppIntent = appIntent;
+ }
+
+ /**
+ * Creates a {@link StatelessBuilder} using an existing {@link Control} as a base.
+ *
+ * @param control base for the builder.
+ */
+ public StatefulBuilder(@NonNull Control control) {
+ Preconditions.checkNotNull(control);
+ mControlId = control.mControlId;
+ mDeviceType = control.mDeviceType;
+ mTitle = control.mTitle;
+ mSubtitle = control.mSubtitle;
+ mStructure = control.mStructure;
+ mZone = control.mZone;
+ mAppIntent = control.mAppIntent;
+ mCustomIcon = control.mCustomIcon;
+ mCustomColor = control.mCustomColor;
+ mStatus = control.mStatus;
+ mControlTemplate = control.mControlTemplate;
+ mStatusText = control.mStatusText;
+ }
+
+ /**
+ * @param controlId the identifier for the {@link Control}.
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setControlId(@NonNull String controlId) {
+ Preconditions.checkNotNull(controlId);
+ mControlId = controlId;
+ return this;
+ }
+
+ /**
+ * @param deviceType type of device represented by this {@link Control}, used to
+ * determine the default icon and color
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setDeviceType(@DeviceTypes.DeviceType int deviceType) {
+ if (!DeviceTypes.validDeviceType(deviceType)) {
+ Log.e(TAG, "Invalid device type:" + deviceType);
+ mDeviceType = DeviceTypes.TYPE_UNKNOWN;
+ } else {
+ mDeviceType = deviceType;
+ }
+ return this;
+ }
+
+ /**
+ * @param title the user facing name of the {@link Control}
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setTitle(@NonNull CharSequence title) {
+ Preconditions.checkNotNull(title);
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * @param subtitle additional information about the {@link Control}, to appear underneath
+ * the title
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setSubtitle(@NonNull CharSequence subtitle) {
+ Preconditions.checkNotNull(subtitle);
+ mSubtitle = subtitle;
+ return this;
+ }
+
+ /**
+ * Optional top-level group to help define the {@link Control}'s location, visible to the
+ * user. If not present, the application name will be used as the top-level group. A
+ * structure contains zones which contains controls.
+ *
+ * @param structure name of the structure containing the control
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setStructure(@Nullable CharSequence structure) {
+ mStructure = structure;
+ return this;
+ }
+
+ /**
+ * Optional group name to help define the {@link Control}'s location within a structure,
+ * visible to the user. A structure contains zones which contains controls.
+ *
+ * @param zone name of the zone containing the control
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setZone(@Nullable CharSequence zone) {
+ mZone = zone;
+ return this;
+ }
+
+ /**
+ * @param appIntent a {@link PendingIntent} linking to an Activity for the {@link Control}
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setAppIntent(@NonNull PendingIntent appIntent) {
+ Preconditions.checkNotNull(appIntent);
+ mAppIntent = appIntent;
+ return this;
+ }
+
+ /**
+ * Optional icon to be shown with the {@link Control}. It is highly recommended
+ * to let the system default the icon unless the default icon is not suitable.
+ *
+ * @param customIcon icon to show
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setCustomIcon(@Nullable Icon customIcon) {
+ mCustomIcon = customIcon;
+ return this;
+ }
+
+ /**
+ * Optional color to be shown with the {@link Control}. It is highly recommended
+ * to let the system default the color unless the default is not suitable for the
+ * application.
+ *
+ * @param customColor background color to use
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setCustomColor(@Nullable ColorStateList customColor) {
+ mCustomColor = customColor;
+ return this;
+ }
+
+ /**
+ * @param status status of the {@link Control}, used to convey information about the
+ * attempt to fetch the current state
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setStatus(@Status int status) {
+ if (status < 0 || status >= NUM_STATUS) {
+ mStatus = STATUS_UNKNOWN;
+ Log.e(TAG, "Status unknown:" + status);
+ } else {
+ mStatus = status;
+ }
+ return this;
+ }
+
+ /**
+ * @param controlTemplate instance of {@link ControlTemplate}, that defines how the
+ * {@link Control} will behave and what interactions are
+ * available to the user
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setControlTemplate(@NonNull ControlTemplate controlTemplate) {
+ Preconditions.checkNotNull(controlTemplate);
+ mControlTemplate = controlTemplate;
+ return this;
+ }
+
+ /**
+ * @param statusText user-facing text description of the {@link Control}'s status,
+ * describing its current state
+ * @return {@code this}
+ */
+ @NonNull
+ public StatefulBuilder setStatusText(@NonNull CharSequence statusText) {
+ Preconditions.checkNotNull(statusText);
+ mStatusText = statusText;
+ return this;
+ }
+
+ /**
+ * @return a valid {@link Control}
+ */
+ @NonNull
+ public Control build() {
+ return new Control(mControlId,
+ mDeviceType,
+ mTitle,
+ mSubtitle,
+ mStructure,
+ mZone,
+ mAppIntent,
+ mCustomIcon,
+ mCustomColor,
+ mStatus,
+ mControlTemplate,
+ mStatusText);
+ }
+ }
+}
diff --git a/android/service/controls/ControlsProviderService.java b/android/service/controls/ControlsProviderService.java
new file mode 100644
index 0000000..4262c40
--- /dev/null
+++ b/android/service/controls/ControlsProviderService.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2019 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.controls;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.service.controls.actions.ControlAction;
+import android.service.controls.actions.ControlActionWrapper;
+import android.service.controls.templates.ControlTemplate;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+import java.util.concurrent.Flow.Publisher;
+import java.util.concurrent.Flow.Subscriber;
+import java.util.concurrent.Flow.Subscription;
+import java.util.function.Consumer;
+
+/**
+ * Service implementation allowing applications to contribute controls to the
+ * System UI.
+ */
+public abstract class ControlsProviderService extends Service {
+
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_CONTROLS =
+ "android.service.controls.ControlsProviderService";
+
+ /**
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ADD_CONTROL =
+ "android.service.controls.action.ADD_CONTROL";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_CONTROL =
+ "android.service.controls.extra.CONTROL";
+
+ /**
+ * @hide
+ */
+ public static final String CALLBACK_BUNDLE = "CALLBACK_BUNDLE";
+
+ /**
+ * @hide
+ */
+ public static final String CALLBACK_TOKEN = "CALLBACK_TOKEN";
+
+ public static final @NonNull String TAG = "ControlsProviderService";
+
+ private IBinder mToken;
+ private RequestHandler mHandler;
+
+ /**
+ * Publisher for all available controls
+ *
+ * Retrieve all available controls. Use the stateless builder {@link Control.StatelessBuilder}
+ * to build each Control. Call {@link Subscriber#onComplete} when done loading all unique
+ * controls, or {@link Subscriber#onError} for error scenarios. Duplicate Controls will
+ * replace the original.
+ */
+ @NonNull
+ public abstract Publisher<Control> createPublisherForAllAvailable();
+
+ /**
+ * (Optional) Publisher for suggested controls
+ *
+ * The service may be asked to provide a small number of recommended controls, in
+ * order to suggest some controls to the user for favoriting. The controls shall be built using
+ * the stateless builder {@link Control.StatelessBuilder}. The number of controls requested
+ * through {@link Subscription#request} will be limited. Call {@link Subscriber#onComplete}
+ * when done, or {@link Subscriber#onError} for error scenarios.
+ */
+ @Nullable
+ public Publisher<Control> createPublisherForSuggested() {
+ return null;
+ }
+
+ /**
+ * Return a valid Publisher for the given controlIds. This publisher will be asked to provide
+ * updates for the given list of controlIds as long as the {@link Subscription} is valid.
+ * Calls to {@link Subscriber#onComplete} will not be expected. Instead, wait for the call from
+ * {@link Subscription#cancel} to indicate that updates are no longer required. It is expected
+ * that controls provided by this publisher were created using {@link Control.StatefulBuilder}.
+ */
+ @NonNull
+ public abstract Publisher<Control> createPublisherFor(@NonNull List<String> controlIds);
+
+ /**
+ * The user has interacted with a Control. The action is dictated by the type of
+ * {@link ControlAction} that was sent. A response can be sent via
+ * {@link Consumer#accept}, with the Integer argument being one of the provided
+ * {@link ControlAction.ResponseResult}. The Integer should indicate whether the action
+ * was received successfully, or if additional prompts should be presented to
+ * the user. Any visual control updates should be sent via the Publisher.
+ */
+ public abstract void performControlAction(@NonNull String controlId,
+ @NonNull ControlAction action, @NonNull Consumer<Integer> consumer);
+
+ @Override
+ @NonNull
+ public final IBinder onBind(@NonNull Intent intent) {
+ mHandler = new RequestHandler(Looper.getMainLooper());
+
+ Bundle bundle = intent.getBundleExtra(CALLBACK_BUNDLE);
+ mToken = bundle.getBinder(CALLBACK_TOKEN);
+
+ return new IControlsProvider.Stub() {
+ public void load(IControlsSubscriber subscriber) {
+ mHandler.obtainMessage(RequestHandler.MSG_LOAD, subscriber).sendToTarget();
+ }
+
+ public void loadSuggested(IControlsSubscriber subscriber) {
+ mHandler.obtainMessage(RequestHandler.MSG_LOAD_SUGGESTED, subscriber)
+ .sendToTarget();
+ }
+
+ public void subscribe(List<String> controlIds,
+ IControlsSubscriber subscriber) {
+ SubscribeMessage msg = new SubscribeMessage(controlIds, subscriber);
+ mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, msg).sendToTarget();
+ }
+
+ public void action(String controlId, ControlActionWrapper action,
+ IControlsActionCallback cb) {
+ ActionMessage msg = new ActionMessage(controlId, action.getWrappedAction(), cb);
+ mHandler.obtainMessage(RequestHandler.MSG_ACTION, msg).sendToTarget();
+ }
+ };
+ }
+
+ @Override
+ public final boolean onUnbind(@NonNull Intent intent) {
+ mHandler = null;
+ return true;
+ }
+
+ private class RequestHandler extends Handler {
+ private static final int MSG_LOAD = 1;
+ private static final int MSG_SUBSCRIBE = 2;
+ private static final int MSG_ACTION = 3;
+ private static final int MSG_LOAD_SUGGESTED = 4;
+
+ RequestHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case MSG_LOAD: {
+ final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
+ final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);
+
+ ControlsProviderService.this.createPublisherForAllAvailable().subscribe(proxy);
+ break;
+ }
+
+ case MSG_LOAD_SUGGESTED: {
+ final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
+ final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);
+
+ Publisher<Control> publisher =
+ ControlsProviderService.this.createPublisherForSuggested();
+ if (publisher == null) {
+ Log.i(TAG, "No publisher provided for suggested controls");
+ proxy.onComplete();
+ } else {
+ publisher.subscribe(proxy);
+ }
+ break;
+ }
+
+ case MSG_SUBSCRIBE: {
+ final SubscribeMessage sMsg = (SubscribeMessage) msg.obj;
+ final SubscriberProxy proxy = new SubscriberProxy(false, mToken,
+ sMsg.mSubscriber);
+
+ ControlsProviderService.this.createPublisherFor(sMsg.mControlIds)
+ .subscribe(proxy);
+ break;
+ }
+
+ case MSG_ACTION: {
+ final ActionMessage aMsg = (ActionMessage) msg.obj;
+ ControlsProviderService.this.performControlAction(aMsg.mControlId,
+ aMsg.mAction, consumerFor(aMsg.mControlId, aMsg.mCb));
+ break;
+ }
+ }
+ }
+
+ private Consumer<Integer> consumerFor(final String controlId,
+ final IControlsActionCallback cb) {
+ return (@NonNull Integer response) -> {
+ Preconditions.checkNotNull(response);
+ if (!ControlAction.isValidResponse(response)) {
+ Log.e(TAG, "Not valid response result: " + response);
+ response = ControlAction.RESPONSE_UNKNOWN;
+ }
+ try {
+ cb.accept(mToken, controlId, response);
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ };
+ }
+ }
+
+ private static boolean isStatelessControl(Control control) {
+ return (control.getStatus() == Control.STATUS_UNKNOWN
+ && control.getControlTemplate().getTemplateType()
+ == ControlTemplate.TYPE_NO_TEMPLATE
+ && TextUtils.isEmpty(control.getStatusText()));
+ }
+
+ private static class SubscriberProxy implements Subscriber<Control> {
+ private IBinder mToken;
+ private IControlsSubscriber mCs;
+ private boolean mEnforceStateless;
+
+ SubscriberProxy(boolean enforceStateless, IBinder token, IControlsSubscriber cs) {
+ mEnforceStateless = enforceStateless;
+ mToken = token;
+ mCs = cs;
+ }
+
+ public void onSubscribe(Subscription subscription) {
+ try {
+ mCs.onSubscribe(mToken, new SubscriptionAdapter(subscription));
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ }
+ public void onNext(@NonNull Control control) {
+ Preconditions.checkNotNull(control);
+ try {
+ if (mEnforceStateless && !isStatelessControl(control)) {
+ Log.w(TAG, "onNext(): control is not stateless. Use the "
+ + "Control.StatelessBuilder() to build the control.");
+ control = new Control.StatelessBuilder(control).build();
+ }
+ mCs.onNext(mToken, control);
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ }
+ public void onError(Throwable t) {
+ try {
+ mCs.onError(mToken, t.toString());
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ }
+ public void onComplete() {
+ try {
+ mCs.onComplete(mToken);
+ } catch (RemoteException ex) {
+ ex.rethrowAsRuntimeException();
+ }
+ }
+ }
+
+ /**
+ * Request SystemUI to prompt the user to add a control to favorites.
+ *
+ * @param context A context
+ * @param componentName Component name of the {@link ControlsProviderService}
+ * @param control A stateless control to show to the user
+ */
+ public static void requestAddControl(@NonNull Context context,
+ @NonNull ComponentName componentName,
+ @NonNull Control control) {
+ Preconditions.checkNotNull(context);
+ Preconditions.checkNotNull(componentName);
+ Preconditions.checkNotNull(control);
+ final String controlsPackage = context.getString(
+ com.android.internal.R.string.config_controlsPackage);
+ Intent intent = new Intent(ACTION_ADD_CONTROL);
+ intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName);
+ intent.setPackage(controlsPackage);
+ if (isStatelessControl(control)) {
+ intent.putExtra(EXTRA_CONTROL, control);
+ } else {
+ intent.putExtra(EXTRA_CONTROL, new Control.StatelessBuilder(control).build());
+ }
+ context.sendBroadcast(intent, Manifest.permission.BIND_CONTROLS);
+ }
+
+ private static class SubscriptionAdapter extends IControlsSubscription.Stub {
+ final Subscription mSubscription;
+
+ SubscriptionAdapter(Subscription s) {
+ this.mSubscription = s;
+ }
+
+ public void request(long n) {
+ mSubscription.request(n);
+ }
+
+ public void cancel() {
+ mSubscription.cancel();
+ }
+ }
+
+ private static class ActionMessage {
+ final String mControlId;
+ final ControlAction mAction;
+ final IControlsActionCallback mCb;
+
+ ActionMessage(String controlId, ControlAction action, IControlsActionCallback cb) {
+ this.mControlId = controlId;
+ this.mAction = action;
+ this.mCb = cb;
+ }
+ }
+
+ private static class SubscribeMessage {
+ final List<String> mControlIds;
+ final IControlsSubscriber mSubscriber;
+
+ SubscribeMessage(List<String> controlIds, IControlsSubscriber subscriber) {
+ this.mControlIds = controlIds;
+ this.mSubscriber = subscriber;
+ }
+ }
+}
diff --git a/android/service/controls/DeviceTypes.java b/android/service/controls/DeviceTypes.java
new file mode 100644
index 0000000..f973610
--- /dev/null
+++ b/android/service/controls/DeviceTypes.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2019 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.controls;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Device types for {@link Control}.
+ *
+ * Each {@link Control} declares a type for the device they represent. This type will be used to
+ * determine icons and colors.
+ * <p>
+ * The type of the device may change on status updates of the {@link Control}. For example, a
+ * device of {@link #TYPE_OUTLET} could be determined by the {@link ControlsProviderService} to be
+ * a {@link #TYPE_COFFEE_MAKER} and change the type for that {@link Control}, therefore possibly
+ * changing icon and color.
+ * <p>
+ * In case the device type is not know by the application but the basic function is, or there is no
+ * provided type, one of the generic types (those starting with {@code TYPE_GENERIC}) can be used.
+ * These will provide an identifiable icon based on the basic function of the device.
+ */
+public class DeviceTypes {
+
+ // Update this when adding new concrete types. Does not count TYPE_UNKNOWN
+ private static final int NUM_CONCRETE_TYPES = 52;
+
+ public static final @DeviceType int TYPE_UNKNOWN = 0;
+ public static final @DeviceType int TYPE_AC_HEATER = 1;
+ public static final @DeviceType int TYPE_AC_UNIT = 2;
+ public static final @DeviceType int TYPE_AIR_FRESHENER = 3;
+ public static final @DeviceType int TYPE_AIR_PURIFIER = 4;
+ public static final @DeviceType int TYPE_COFFEE_MAKER = 5;
+ public static final @DeviceType int TYPE_DEHUMIDIFIER = 6;
+ public static final @DeviceType int TYPE_DISPLAY = 7;
+ public static final @DeviceType int TYPE_FAN = 8;
+ public static final @DeviceType int TYPE_HOOD = 10;
+ public static final @DeviceType int TYPE_HUMIDIFIER = 11;
+ public static final @DeviceType int TYPE_KETTLE = 12;
+ public static final @DeviceType int TYPE_LIGHT = 13;
+ public static final @DeviceType int TYPE_MICROWAVE = 14;
+ public static final @DeviceType int TYPE_OUTLET = 15;
+ public static final @DeviceType int TYPE_RADIATOR = 16;
+ public static final @DeviceType int TYPE_REMOTE_CONTROL = 17;
+ public static final @DeviceType int TYPE_SET_TOP = 18;
+ public static final @DeviceType int TYPE_STANDMIXER = 19;
+ public static final @DeviceType int TYPE_STYLER = 20;
+ public static final @DeviceType int TYPE_SWITCH = 21;
+ public static final @DeviceType int TYPE_TV = 22;
+ public static final @DeviceType int TYPE_WATER_HEATER = 23;
+
+ public static final @DeviceType int TYPE_DISHWASHER = 24;
+ public static final @DeviceType int TYPE_DRYER = 25;
+ public static final @DeviceType int TYPE_MOP = 26;
+ public static final @DeviceType int TYPE_MOWER = 27;
+ public static final @DeviceType int TYPE_MULTICOOKER = 28;
+ public static final @DeviceType int TYPE_SHOWER = 29;
+ public static final @DeviceType int TYPE_SPRINKLER = 30;
+ public static final @DeviceType int TYPE_WASHER = 31;
+ public static final @DeviceType int TYPE_VACUUM = 32;
+
+ public static final @DeviceType int TYPE_AWNING = 33;
+ public static final @DeviceType int TYPE_BLINDS = 34;
+ public static final @DeviceType int TYPE_CLOSET = 35;
+ public static final @DeviceType int TYPE_CURTAIN = 36;
+ public static final @DeviceType int TYPE_DOOR = 37;
+ public static final @DeviceType int TYPE_DRAWER = 38;
+ public static final @DeviceType int TYPE_GARAGE = 39;
+ public static final @DeviceType int TYPE_GATE = 40;
+ public static final @DeviceType int TYPE_PERGOLA = 41;
+ public static final @DeviceType int TYPE_SHUTTER = 42;
+ public static final @DeviceType int TYPE_WINDOW = 43;
+ public static final @DeviceType int TYPE_VALVE = 44;
+
+ public static final @DeviceType int TYPE_LOCK = 45;
+
+ public static final @DeviceType int TYPE_SECURITY_SYSTEM = 46;
+
+ public static final @DeviceType int TYPE_HEATER = 47;
+ public static final @DeviceType int TYPE_REFRIGERATOR = 48;
+ public static final @DeviceType int TYPE_THERMOSTAT = 49;
+
+ public static final @DeviceType int TYPE_CAMERA = 50;
+ public static final @DeviceType int TYPE_DOORBELL = 51;
+
+ /*
+ * Also known as macros, routines can aggregate a series of actions across multiple devices
+ */
+ public static final @DeviceType int TYPE_ROUTINE = 52;
+
+ // Update this when adding new generic types.
+ private static final int NUM_GENERIC_TYPES = 7;
+ public static final @DeviceType int TYPE_GENERIC_ON_OFF = -1;
+ public static final @DeviceType int TYPE_GENERIC_START_STOP = -2;
+ public static final @DeviceType int TYPE_GENERIC_OPEN_CLOSE = -3;
+ public static final @DeviceType int TYPE_GENERIC_LOCK_UNLOCK = -4;
+ public static final @DeviceType int TYPE_GENERIC_ARM_DISARM = -5;
+ public static final @DeviceType int TYPE_GENERIC_TEMP_SETTING = -6;
+ public static final @DeviceType int TYPE_GENERIC_VIEWSTREAM = -7;
+
+ public static boolean validDeviceType(int deviceType) {
+ return deviceType >= -NUM_GENERIC_TYPES && deviceType <= NUM_CONCRETE_TYPES;
+ }
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ TYPE_GENERIC_ON_OFF,
+ TYPE_GENERIC_START_STOP,
+ TYPE_GENERIC_OPEN_CLOSE,
+ TYPE_GENERIC_LOCK_UNLOCK,
+ TYPE_GENERIC_ARM_DISARM,
+ TYPE_GENERIC_TEMP_SETTING,
+ TYPE_GENERIC_VIEWSTREAM,
+
+ TYPE_UNKNOWN,
+
+ TYPE_AC_HEATER,
+ TYPE_AC_UNIT,
+ TYPE_AIR_FRESHENER,
+ TYPE_AIR_PURIFIER,
+ TYPE_COFFEE_MAKER,
+ TYPE_DEHUMIDIFIER,
+ TYPE_DISPLAY,
+ TYPE_FAN,
+ TYPE_HOOD,
+ TYPE_HUMIDIFIER,
+ TYPE_KETTLE,
+ TYPE_LIGHT,
+ TYPE_MICROWAVE,
+ TYPE_OUTLET,
+ TYPE_RADIATOR,
+ TYPE_REMOTE_CONTROL,
+ TYPE_SET_TOP,
+ TYPE_STANDMIXER,
+ TYPE_STYLER,
+ TYPE_SWITCH,
+ TYPE_TV,
+ TYPE_WATER_HEATER,
+ TYPE_DISHWASHER,
+ TYPE_DRYER,
+ TYPE_MOP,
+ TYPE_MOWER,
+ TYPE_MULTICOOKER,
+ TYPE_SHOWER,
+ TYPE_SPRINKLER,
+ TYPE_WASHER,
+ TYPE_VACUUM,
+ TYPE_AWNING,
+ TYPE_BLINDS,
+ TYPE_CLOSET,
+ TYPE_CURTAIN,
+ TYPE_DOOR,
+ TYPE_DRAWER,
+ TYPE_GARAGE,
+ TYPE_GATE,
+ TYPE_PERGOLA,
+ TYPE_SHUTTER,
+ TYPE_WINDOW,
+ TYPE_VALVE,
+ TYPE_LOCK,
+ TYPE_SECURITY_SYSTEM,
+ TYPE_HEATER,
+ TYPE_REFRIGERATOR,
+ TYPE_THERMOSTAT,
+ TYPE_CAMERA,
+ TYPE_DOORBELL,
+ TYPE_ROUTINE
+ })
+ public @interface DeviceType {}
+
+ private DeviceTypes() {}
+}
diff --git a/android/service/controls/actions/BooleanAction.java b/android/service/controls/actions/BooleanAction.java
new file mode 100644
index 0000000..b794ead
--- /dev/null
+++ b/android/service/controls/actions/BooleanAction.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 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.controls.actions;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.service.controls.Control;
+import android.service.controls.templates.ToggleRangeTemplate;
+import android.service.controls.templates.ToggleTemplate;
+
+/**
+ * Action sent by user toggling a {@link Control} between checked/unchecked.
+ *
+ * This action is available when the {@link Control} was constructed with either a
+ * {@link ToggleTemplate} or a {@link ToggleRangeTemplate}.
+ */
+public final class BooleanAction extends ControlAction {
+
+ private static final @ActionType int TYPE = TYPE_BOOLEAN;
+ private static final String KEY_NEW_STATE = "key_new_state";
+
+ private final boolean mNewState;
+
+ /**
+ * @param templateId the identifier of the {@link ToggleTemplate} that produced this action.
+ * @param newState new value for the state displayed by the {@link ToggleTemplate}.
+ */
+ public BooleanAction(@NonNull String templateId, boolean newState) {
+ this(templateId, newState, null);
+ }
+
+ /**
+ * @param templateId the identifier of the template that originated this action.
+ * @param newState new value for the state displayed by the template.
+ * @param challengeValue a value sent by the user along with the action to authenticate. {@code}
+ * null is sent when no authentication is needed or has not been
+ * requested.
+ */
+ public BooleanAction(@NonNull String templateId, boolean newState,
+ @Nullable String challengeValue) {
+ super(templateId, challengeValue);
+ mNewState = newState;
+ }
+
+ /**
+ * @param b
+ * @hide
+ */
+ BooleanAction(Bundle b) {
+ super(b);
+ mNewState = b.getBoolean(KEY_NEW_STATE);
+ }
+
+ /**
+ * The new state set for the button in the corresponding {@link ToggleTemplate}.
+ *
+ * @return {@code true} if the button was toggled from unchecked to checked.
+ */
+ public boolean getNewState() {
+ return mNewState;
+ }
+
+ /**
+ * @return {@link ControlAction#TYPE_BOOLEAN}
+ */
+ @Override
+ public int getActionType() {
+ return TYPE;
+ }
+
+ /**
+ * @return
+ * @hide
+ */
+ @Override
+ @NonNull
+ Bundle getDataBundle() {
+ Bundle b = super.getDataBundle();
+ b.putBoolean(KEY_NEW_STATE, mNewState);
+ return b;
+ }
+}
diff --git a/android/service/controls/actions/CommandAction.java b/android/service/controls/actions/CommandAction.java
new file mode 100644
index 0000000..a560fa4
--- /dev/null
+++ b/android/service/controls/actions/CommandAction.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 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.controls.actions;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.service.controls.Control;
+import android.service.controls.templates.StatelessTemplate;
+
+/**
+ * A simple {@link ControlAction} indicating that the user has interacted with a {@link Control}
+ * created using a {@link StatelessTemplate}.
+ */
+public final class CommandAction extends ControlAction {
+
+ private static final @ActionType int TYPE = TYPE_COMMAND;
+
+ /**
+ * @param templateId the identifier of the {@link StatelessTemplate} that originated this
+ * action.
+ * @param challengeValue a value sent by the user along with the action to authenticate. {@code}
+ * null is sent when no authentication is needed or has not been
+ * requested.
+ */
+ public CommandAction(@NonNull String templateId, @Nullable String challengeValue) {
+ super(templateId, challengeValue);
+ }
+
+ /**
+ * @param templateId the identifier of the {@link StatelessTemplate} that originated this
+ * action.
+ */
+ public CommandAction(@NonNull String templateId) {
+ this(templateId, null);
+ }
+
+ /**
+ * @param b
+ * @hide
+ */
+ CommandAction(Bundle b) {
+ super(b);
+ }
+
+ /**
+ * @return {@link ControlAction#TYPE_COMMAND}
+ */
+ @Override
+ public int getActionType() {
+ return TYPE;
+ }
+}
diff --git a/android/service/controls/actions/ControlAction.java b/android/service/controls/actions/ControlAction.java
new file mode 100644
index 0000000..10f526d
--- /dev/null
+++ b/android/service/controls/actions/ControlAction.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2019 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.controls.actions;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.service.controls.Control;
+import android.service.controls.ControlsProviderService;
+import android.service.controls.templates.ControlTemplate;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An abstract action indicating a user interaction with a {@link Control}.
+ *
+ * In some cases, an action needs to be validated by the user, using a password, PIN or simple
+ * acknowledgment. For those cases, an optional (nullable) parameter can be passed to send the user
+ * input. This <b>challenge value</b> will be requested from the user and sent as part
+ * of a {@link ControlAction} only if the service has responded to an action with one of:
+ * <ul>
+ * <li> {@link #RESPONSE_CHALLENGE_ACK}
+ * <li> {@link #RESPONSE_CHALLENGE_PIN}
+ * <li> {@link #RESPONSE_CHALLENGE_PASSPHRASE}
+ * </ul>
+ */
+public abstract class ControlAction {
+
+ private static final String TAG = "ControlAction";
+
+ private static final String KEY_ACTION_TYPE = "key_action_type";
+ private static final String KEY_TEMPLATE_ID = "key_template_id";
+ private static final String KEY_CHALLENGE_VALUE = "key_challenge_value";
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ TYPE_ERROR,
+ TYPE_BOOLEAN,
+ TYPE_FLOAT,
+ TYPE_MODE,
+ TYPE_COMMAND
+ })
+ public @interface ActionType {};
+
+ /**
+ * Object returned when there is an unparcelling error.
+ * @hide
+ */
+ public static final @NonNull ControlAction ERROR_ACTION = new ControlAction() {
+ @Override
+ public int getActionType() {
+ return TYPE_ERROR;
+ }
+ };
+
+ /**
+ * The identifier of the action returned by {@link #getErrorAction}.
+ */
+ public static final @ActionType int TYPE_ERROR = -1;
+
+ /**
+ * The identifier of {@link BooleanAction}.
+ */
+ public static final @ActionType int TYPE_BOOLEAN = 1;
+
+ /**
+ * The identifier of {@link FloatAction}.
+ */
+ public static final @ActionType int TYPE_FLOAT = 2;
+
+ /**
+ * The identifier of {@link ModeAction}.
+ */
+ public static final @ActionType int TYPE_MODE = 4;
+
+ /**
+ * The identifier of {@link CommandAction}.
+ */
+ public static final @ActionType int TYPE_COMMAND = 5;
+
+
+ public static final boolean isValidResponse(@ResponseResult int response) {
+ return (response >= 0 && response < NUM_RESPONSE_TYPES);
+ }
+ private static final int NUM_RESPONSE_TYPES = 6;
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ RESPONSE_UNKNOWN,
+ RESPONSE_OK,
+ RESPONSE_FAIL,
+ RESPONSE_CHALLENGE_ACK,
+ RESPONSE_CHALLENGE_PIN,
+ RESPONSE_CHALLENGE_PASSPHRASE
+ })
+ public @interface ResponseResult {};
+
+ public static final @ResponseResult int RESPONSE_UNKNOWN = 0;
+
+ /**
+ * Response code for the {@code consumer} in
+ * {@link ControlsProviderService#performControlAction} indicating that the action has been
+ * performed. The action may still fail later and the state may not change.
+ */
+ public static final @ResponseResult int RESPONSE_OK = 1;
+ /**
+ * Response code for the {@code consumer} in
+ * {@link ControlsProviderService#performControlAction} indicating that the action has failed.
+ */
+ public static final @ResponseResult int RESPONSE_FAIL = 2;
+ /**
+ * Response code for the {@code consumer} in
+ * {@link ControlsProviderService#performControlAction} indicating that in order for the action
+ * to be performed, acknowledgment from the user is required. Any non-empty string returned
+ * from {@link #getChallengeValue} shall be treated as a positive acknowledgment.
+ */
+ public static final @ResponseResult int RESPONSE_CHALLENGE_ACK = 3;
+ /**
+ * Response code for the {@code consumer} in
+ * {@link ControlsProviderService#performControlAction} indicating that in order for the action
+ * to be performed, a PIN is required.
+ */
+ public static final @ResponseResult int RESPONSE_CHALLENGE_PIN = 4;
+ /**
+ * Response code for the {@code consumer} in
+ * {@link ControlsProviderService#performControlAction} indicating that in order for the action
+ * to be performed, an alphanumeric passphrase is required.
+ */
+ public static final @ResponseResult int RESPONSE_CHALLENGE_PASSPHRASE = 5;
+
+ /**
+ * The {@link ActionType} associated with this class.
+ */
+ public abstract @ActionType int getActionType();
+
+ private final @NonNull String mTemplateId;
+ private final @Nullable String mChallengeValue;
+
+ private ControlAction() {
+ mTemplateId = "";
+ mChallengeValue = null;
+ }
+
+ /**
+ * @hide
+ */
+ ControlAction(@NonNull String templateId, @Nullable String challengeValue) {
+ Preconditions.checkNotNull(templateId);
+ mTemplateId = templateId;
+ mChallengeValue = challengeValue;
+ }
+
+ /**
+ * @hide
+ */
+ ControlAction(Bundle b) {
+ mTemplateId = b.getString(KEY_TEMPLATE_ID);
+ mChallengeValue = b.getString(KEY_CHALLENGE_VALUE);
+ }
+
+ /**
+ * The identifier of the {@link ControlTemplate} that originated this action
+ */
+ @NonNull
+ public String getTemplateId() {
+ return mTemplateId;
+ }
+
+ /**
+ * The challenge value used to authenticate certain actions, if available.
+ */
+ @Nullable
+ public String getChallengeValue() {
+ return mChallengeValue;
+ }
+
+ /**
+ * Obtain a {@link Bundle} describing this object populated with data.
+ *
+ * Implementations in subclasses should populate the {@link Bundle} returned by
+ * {@link ControlAction}.
+ * @return a {@link Bundle} containing the data that represents this object.
+ * @hide
+ */
+ @CallSuper
+ @NonNull
+ Bundle getDataBundle() {
+ Bundle b = new Bundle();
+ b.putInt(KEY_ACTION_TYPE, getActionType());
+ b.putString(KEY_TEMPLATE_ID, mTemplateId);
+ b.putString(KEY_CHALLENGE_VALUE, mChallengeValue);
+ return b;
+ }
+
+ /**
+ * @param bundle
+ * @return
+ * @hide
+ */
+ @NonNull
+ static ControlAction createActionFromBundle(@NonNull Bundle bundle) {
+ if (bundle == null) {
+ Log.e(TAG, "Null bundle");
+ return ERROR_ACTION;
+ }
+ int type = bundle.getInt(KEY_ACTION_TYPE, TYPE_ERROR);
+ try {
+ switch (type) {
+ case TYPE_BOOLEAN:
+ return new BooleanAction(bundle);
+ case TYPE_FLOAT:
+ return new FloatAction(bundle);
+ case TYPE_MODE:
+ return new ModeAction(bundle);
+ case TYPE_COMMAND:
+ return new CommandAction(bundle);
+ case TYPE_ERROR:
+ default:
+ return ERROR_ACTION;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error creating action", e);
+ return ERROR_ACTION;
+ }
+ }
+
+ /**
+ * Returns a singleton {@link ControlAction} used for indicating an error in unparceling.
+ */
+ @NonNull
+ public static ControlAction getErrorAction() {
+ return ERROR_ACTION;
+ }
+}
diff --git a/android/service/controls/actions/ControlActionWrapper.java b/android/service/controls/actions/ControlActionWrapper.java
new file mode 100644
index 0000000..6a3ec86
--- /dev/null
+++ b/android/service/controls/actions/ControlActionWrapper.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 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.controls.actions;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Wrapper for parceling/unparceling {@link ControlAction}.
+ * @hide
+ */
+public final class ControlActionWrapper implements Parcelable {
+
+ private final @NonNull ControlAction mControlAction;
+
+ public ControlActionWrapper(@NonNull ControlAction controlAction) {
+ Preconditions.checkNotNull(controlAction);
+
+ mControlAction = controlAction;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBundle(mControlAction.getDataBundle());
+ }
+
+ @NonNull
+ public ControlAction getWrappedAction() {
+ return mControlAction;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<ControlActionWrapper> CREATOR =
+ new Creator<ControlActionWrapper>() {
+ @Override
+ public ControlActionWrapper createFromParcel(@NonNull Parcel in) {
+ return new ControlActionWrapper(
+ ControlAction.createActionFromBundle(in.readBundle()));
+ }
+
+ @Override
+ public ControlActionWrapper[] newArray(int size) {
+ return new ControlActionWrapper[size];
+ }
+ };
+}
diff --git a/android/service/controls/actions/FloatAction.java b/android/service/controls/actions/FloatAction.java
new file mode 100644
index 0000000..5b271ce
--- /dev/null
+++ b/android/service/controls/actions/FloatAction.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 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.controls.actions;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.service.controls.templates.RangeTemplate;
+import android.service.controls.templates.ToggleRangeTemplate;
+
+/**
+ * Action sent by a {@link RangeTemplate}, {@link ToggleRangeTemplate}.
+ */
+public final class FloatAction extends ControlAction {
+
+ private static final @ActionType int TYPE = TYPE_FLOAT;
+ private static final String KEY_NEW_VALUE = "key_new_value";
+
+ private final float mNewValue;
+
+ /**
+ * @param templateId the identifier of the {@link RangeTemplate} that produced this action.
+ * @param newValue new value for the state displayed by the {@link RangeTemplate}.
+ */
+ public FloatAction(@NonNull String templateId, float newValue) {
+ this(templateId, newValue, null);
+ }
+
+ /**
+ * @param templateId the identifier of the {@link RangeTemplate} that originated this action.
+ * @param newValue new value for the state of the {@link RangeTemplate}.
+ * @param challengeValue a value sent by the user along with the action to authenticate. {@code}
+ * null is sent when no authentication is needed or has not been
+ * requested.
+ */
+
+ public FloatAction(@NonNull String templateId, float newValue,
+ @Nullable String challengeValue) {
+ super(templateId, challengeValue);
+ mNewValue = newValue;
+ }
+
+ /**
+ * @param b
+ * @hide
+ */
+ FloatAction(Bundle b) {
+ super(b);
+ mNewValue = b.getFloat(KEY_NEW_VALUE);
+ }
+
+ /**
+ * The new value set for the range in the corresponding {@link RangeTemplate}.
+ */
+ public float getNewValue() {
+ return mNewValue;
+ }
+
+ /**
+ * @return {@link ControlAction#TYPE_FLOAT}
+ */
+ @Override
+ public int getActionType() {
+ return TYPE;
+ }
+
+ /**
+ * @return
+ * @hide
+ */
+ @Override
+ @NonNull
+ Bundle getDataBundle() {
+ Bundle b = super.getDataBundle();
+ b.putFloat(KEY_NEW_VALUE, mNewValue);
+ return b;
+ }
+}
diff --git a/android/service/controls/actions/ModeAction.java b/android/service/controls/actions/ModeAction.java
new file mode 100644
index 0000000..c0e24ad
--- /dev/null
+++ b/android/service/controls/actions/ModeAction.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 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.controls.actions;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.service.controls.Control;
+import android.service.controls.templates.TemperatureControlTemplate;
+
+/**
+ * Action sent by the user to indicate a change of mode.
+ *
+ * This action is available when the {@link Control} was created with a
+ * {@link TemperatureControlTemplate}.
+ */
+public final class ModeAction extends ControlAction {
+
+ private static final @ActionType int TYPE = TYPE_MODE;
+ private static final String KEY_MODE = "key_mode";
+
+ private final int mNewMode;
+
+ /**
+ * @return {@link ControlAction#TYPE_MODE}.
+ */
+ @Override
+ public int getActionType() {
+ return TYPE;
+ }
+
+ /**
+ * @param templateId the identifier of the {@link TemperatureControlTemplate} that originated
+ * this action.
+ * @param newMode new value for the mode.
+ * @param challengeValue a value sent by the user along with the action to authenticate. {@code}
+ * null is sent when no authentication is needed or has not been
+ * requested.
+ */
+ public ModeAction(@NonNull String templateId, int newMode, @Nullable String challengeValue) {
+ super(templateId, challengeValue);
+ mNewMode = newMode;
+ }
+
+ /**
+ * @param templateId the identifier of the {@link TemperatureControlTemplate} that originated
+ * this action.
+ * @param newMode new value for the mode.
+ */
+ public ModeAction(@NonNull String templateId, int newMode) {
+ this(templateId, newMode, null);
+ }
+
+ /**
+ * @param b
+ * @hide
+ */
+ ModeAction(Bundle b) {
+ super(b);
+ mNewMode = b.getInt(KEY_MODE);
+ }
+
+ /**
+ * @return
+ * @hide
+ */
+ @Override
+ @NonNull
+ Bundle getDataBundle() {
+ Bundle b = super.getDataBundle();
+ b.putInt(KEY_MODE, mNewMode);
+ return b;
+ }
+
+ public int getNewMode() {
+ return mNewMode;
+ }
+}
diff --git a/android/service/controls/templates/ControlButton.java b/android/service/controls/templates/ControlButton.java
new file mode 100644
index 0000000..157e231
--- /dev/null
+++ b/android/service/controls/templates/ControlButton.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 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.controls.templates;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Button element for {@link ControlTemplate}.
+ */
+public final class ControlButton implements Parcelable {
+
+ private final boolean mChecked;
+ private final @NonNull CharSequence mActionDescription;
+
+ /**
+ * @param checked true if the button should be rendered as active.
+ * @param actionDescription action description for the button.
+ */
+ public ControlButton(boolean checked,
+ @NonNull CharSequence actionDescription) {
+ Preconditions.checkNotNull(actionDescription);
+ mChecked = checked;
+ mActionDescription = actionDescription;
+ }
+
+ /**
+ * Whether the button should be rendered in a checked state.
+ */
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ /**
+ * The content description for this button.
+ */
+ @NonNull
+ public CharSequence getActionDescription() {
+ return mActionDescription;
+ }
+
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ @NonNull
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeByte(mChecked ? (byte) 1 : (byte) 0);
+ dest.writeCharSequence(mActionDescription);
+ }
+
+ ControlButton(Parcel in) {
+ mChecked = in.readByte() != 0;
+ mActionDescription = in.readCharSequence();
+ }
+
+ public static final @NonNull Creator<ControlButton> CREATOR = new Creator<ControlButton>() {
+ @Override
+ public ControlButton createFromParcel(Parcel source) {
+ return new ControlButton(source);
+ }
+
+ @Override
+ public ControlButton[] newArray(int size) {
+ return new ControlButton[size];
+ }
+ };
+}
diff --git a/android/service/controls/templates/ControlTemplate.java b/android/service/controls/templates/ControlTemplate.java
new file mode 100644
index 0000000..1e16273
--- /dev/null
+++ b/android/service/controls/templates/ControlTemplate.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2019 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.controls.templates;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.service.controls.Control;
+import android.service.controls.actions.ControlAction;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An abstract input template for a {@link Control}.
+ *
+ * Specifies what layout is presented to the user for a given {@link Control}.
+ * <p>
+ * Some instances of {@link Control} can originate actions (via user interaction) to modify its
+ * associated state. The actions available to a given {@link Control} are determined by its
+ * {@link ControlTemplate}.
+ * @see ControlAction
+ */
+public abstract class ControlTemplate {
+
+ private static final String TAG = "ControlTemplate";
+
+ private static final String KEY_TEMPLATE_ID = "key_template_id";
+ private static final String KEY_TEMPLATE_TYPE = "key_template_type";
+
+ /**
+ * Singleton representing a {@link Control} with no input.
+ * @hide
+ */
+ public static final @NonNull ControlTemplate NO_TEMPLATE = new ControlTemplate("") {
+ @Override
+ public int getTemplateType() {
+ return TYPE_NO_TEMPLATE;
+ }
+ };
+
+ /**
+ * Object returned when there is an unparcelling error.
+ * @hide
+ */
+ private static final @NonNull ControlTemplate ERROR_TEMPLATE = new ControlTemplate("") {
+ @Override
+ public int getTemplateType() {
+ return TYPE_ERROR;
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ TYPE_ERROR,
+ TYPE_NO_TEMPLATE,
+ TYPE_TOGGLE,
+ TYPE_RANGE,
+ TYPE_TOGGLE_RANGE,
+ TYPE_TEMPERATURE,
+ TYPE_STATELESS
+ })
+ public @interface TemplateType {}
+
+ /**
+ * Type identifier of the template returned by {@link #getErrorTemplate()}.
+ */
+ public static final @TemplateType int TYPE_ERROR = -1;
+
+ /**
+ * Type identifier of {@link ControlTemplate#getNoTemplateObject}.
+ */
+ public static final @TemplateType int TYPE_NO_TEMPLATE = 0;
+
+ /**
+ * Type identifier of {@link ToggleTemplate}.
+ */
+ public static final @TemplateType int TYPE_TOGGLE = 1;
+
+ /**
+ * Type identifier of {@link RangeTemplate}.
+ */
+ public static final @TemplateType int TYPE_RANGE = 2;
+
+ /**
+ * Type identifier of {@link ToggleRangeTemplate}.
+ */
+ public static final @TemplateType int TYPE_TOGGLE_RANGE = 6;
+
+ /**
+ * Type identifier of {@link TemperatureControlTemplate}.
+ */
+ public static final @TemplateType int TYPE_TEMPERATURE = 7;
+
+ /**
+ * Type identifier of {@link StatelessTemplate}.
+ */
+ public static final @TemplateType int TYPE_STATELESS = 8;
+
+ private @NonNull final String mTemplateId;
+
+ /**
+ * @return the identifier for this object.
+ */
+ @NonNull
+ public String getTemplateId() {
+ return mTemplateId;
+ }
+
+ /**
+ * The {@link TemplateType} associated with this class.
+ */
+ public abstract @TemplateType int getTemplateType();
+
+ /**
+ * Obtain a {@link Bundle} describing this object populated with data.
+ * @return a {@link Bundle} containing the data that represents this object.
+ * @hide
+ */
+ @CallSuper
+ @NonNull
+ Bundle getDataBundle() {
+ Bundle b = new Bundle();
+ b.putInt(KEY_TEMPLATE_TYPE, getTemplateType());
+ b.putString(KEY_TEMPLATE_ID, mTemplateId);
+ return b;
+ }
+
+ private ControlTemplate() {
+ mTemplateId = "";
+ }
+
+ /**
+ * @param b
+ * @hide
+ */
+ ControlTemplate(@NonNull Bundle b) {
+ mTemplateId = b.getString(KEY_TEMPLATE_ID);
+ }
+
+ /**
+ * @hide
+ */
+ ControlTemplate(@NonNull String templateId) {
+ Preconditions.checkNotNull(templateId);
+ mTemplateId = templateId;
+ }
+
+ /**
+ *
+ * @param bundle
+ * @return
+ * @hide
+ */
+ @NonNull
+ static ControlTemplate createTemplateFromBundle(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ Log.e(TAG, "Null bundle");
+ return ERROR_TEMPLATE;
+ }
+ int type = bundle.getInt(KEY_TEMPLATE_TYPE, TYPE_ERROR);
+ try {
+ switch (type) {
+ case TYPE_TOGGLE:
+ return new ToggleTemplate(bundle);
+ case TYPE_RANGE:
+ return new RangeTemplate(bundle);
+ case TYPE_TOGGLE_RANGE:
+ return new ToggleRangeTemplate(bundle);
+ case TYPE_TEMPERATURE:
+ return new TemperatureControlTemplate(bundle);
+ case TYPE_STATELESS:
+ return new StatelessTemplate(bundle);
+ case TYPE_NO_TEMPLATE:
+ return NO_TEMPLATE;
+ case TYPE_ERROR:
+ default:
+ return ERROR_TEMPLATE;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error creating template", e);
+ return ERROR_TEMPLATE;
+ }
+ }
+
+ /**
+ * @return a singleton {@link ControlTemplate} used for indicating an error in unparceling.
+ */
+ @NonNull
+ public static ControlTemplate getErrorTemplate() {
+ return ERROR_TEMPLATE;
+ }
+
+ /**
+ * Get a singleton {@link ControlTemplate} that has no features.
+ *
+ * This template has no distinctive field, not even an identifier. Used for a {@link Control}
+ * that accepts no type of input, or when there is no known state.
+ *
+ * @return a singleton {@link ControlTemplate} to indicate no specific template is used by
+ * this {@link Control}
+ */
+ @NonNull
+ public static ControlTemplate getNoTemplateObject() {
+ return NO_TEMPLATE;
+ }
+
+}
diff --git a/android/service/controls/templates/ControlTemplateWrapper.java b/android/service/controls/templates/ControlTemplateWrapper.java
new file mode 100644
index 0000000..7957260
--- /dev/null
+++ b/android/service/controls/templates/ControlTemplateWrapper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 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.controls.templates;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Wrapper for parceling/unparceling {@link ControlTemplate}.
+ * @hide
+ */
+public final class ControlTemplateWrapper implements Parcelable {
+
+ private final @NonNull ControlTemplate mControlTemplate;
+
+ public ControlTemplateWrapper(@NonNull ControlTemplate template) {
+ Preconditions.checkNotNull(template);
+ mControlTemplate = template;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public ControlTemplate getWrappedTemplate() {
+ return mControlTemplate;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBundle(mControlTemplate.getDataBundle());
+ }
+
+ public static final @NonNull Creator<ControlTemplateWrapper> CREATOR =
+ new Creator<ControlTemplateWrapper>() {
+ @Override
+ public ControlTemplateWrapper createFromParcel(@NonNull Parcel source) {
+ return new ControlTemplateWrapper(
+ ControlTemplate.createTemplateFromBundle(source.readBundle()));
+ }
+
+ @Override
+ public ControlTemplateWrapper[] newArray(int size) {
+ return new ControlTemplateWrapper[size];
+ }
+ };
+}
diff --git a/android/service/controls/templates/RangeTemplate.java b/android/service/controls/templates/RangeTemplate.java
new file mode 100644
index 0000000..0d977d3
--- /dev/null
+++ b/android/service/controls/templates/RangeTemplate.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2019 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.controls.templates;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.service.controls.Control;
+import android.service.controls.actions.FloatAction;
+
+/**
+ * A template for a {@link Control} with inputs in a "continuous" range of values.
+ *
+ * @see FloatAction
+ */
+public final class RangeTemplate extends ControlTemplate {
+
+ private static final @TemplateType int TYPE = TYPE_RANGE;
+ private static final String KEY_MIN_VALUE = "key_min_value";
+ private static final String KEY_MAX_VALUE = "key_max_value";
+ private static final String KEY_CURRENT_VALUE = "key_current_value";
+ private static final String KEY_STEP_VALUE = "key_step_value";
+ private static final String KEY_FORMAT_STRING = "key_format_string";
+
+ private final float mMinValue;
+ private final float mMaxValue;
+ private final float mCurrentValue;
+ private final float mStepValue;
+ private final @NonNull CharSequence mFormatString;
+
+ /**
+ * Construct a new {@link RangeTemplate}.
+ *
+ * The range must be valid, meaning:
+ * <ul>
+ * <li> {@code minValue} < {@code maxValue}
+ * <li> {@code minValue} < {@code currentValue}
+ * <li> {@code currentValue} < {@code maxValue}
+ * <li> 0 < {@code stepValue}
+ * </ul>
+ * <p>
+ * The current value of the Control will be formatted accordingly.
+ *
+ * @param templateId the identifier for this template object
+ * @param minValue minimum value for the input
+ * @param maxValue maximum value for the input
+ * @param currentValue the current value of the {@link Control} containing this object.
+ * @param stepValue minimum value of increments/decrements when interacting with this control.
+ * @param formatString a formatting string as per {@link String#format} used to display the
+ * {@code currentValue}. If {@code null} is passed, the "%.1f" is used.
+ * @throws IllegalArgumentException if the parameters passed do not make a valid range.
+ */
+ public RangeTemplate(@NonNull String templateId,
+ float minValue,
+ float maxValue,
+ float currentValue,
+ float stepValue,
+ @Nullable CharSequence formatString) {
+ super(templateId);
+ mMinValue = minValue;
+ mMaxValue = maxValue;
+ mCurrentValue = currentValue;
+ mStepValue = stepValue;
+ if (formatString != null) {
+ mFormatString = formatString;
+ } else {
+ mFormatString = "%.1f";
+ }
+ validate();
+ }
+
+ /**
+ * Construct a new {@link RangeTemplate} from a {@link Bundle}.
+ *
+ * @throws IllegalArgumentException if the parameters passed do not make a valid range
+ * @see RangeTemplate#RangeTemplate(String, float, float, float, float, CharSequence)
+ * @hide
+ */
+ RangeTemplate(Bundle b) {
+ super(b);
+ mMinValue = b.getFloat(KEY_MIN_VALUE);
+ mMaxValue = b.getFloat(KEY_MAX_VALUE);
+ mCurrentValue = b.getFloat(KEY_CURRENT_VALUE);
+ mStepValue = b.getFloat(KEY_STEP_VALUE);
+ mFormatString = b.getCharSequence(KEY_FORMAT_STRING, "%.1f");
+ validate();
+ }
+
+ /**
+ * The minimum value for this range.
+ */
+ public float getMinValue() {
+ return mMinValue;
+ }
+
+ /**
+ * The maximum value for this range.
+ */
+ public float getMaxValue() {
+ return mMaxValue;
+ }
+
+ /**
+ * The current value for this range.
+ */
+ public float getCurrentValue() {
+ return mCurrentValue;
+ }
+
+ /**
+ * The value of the smallest increment or decrement that can be performed on this range.
+ */
+ public float getStepValue() {
+ return mStepValue;
+ }
+
+ /**
+ * Formatter for generating a user visible {@link String} representing the value
+ * returned by {@link RangeTemplate#getCurrentValue}.
+ * @return a formatting string as specified in {@link String#format}
+ */
+ @NonNull
+ public CharSequence getFormatString() {
+ return mFormatString;
+ }
+
+ /**
+ * @return {@link ControlTemplate#TYPE_RANGE}
+ */
+ @Override
+ public int getTemplateType() {
+ return TYPE;
+ }
+
+ /**
+ * @return
+ * @hide
+ */
+ @Override
+ @NonNull
+ Bundle getDataBundle() {
+ Bundle b = super.getDataBundle();
+ b.putFloat(KEY_MIN_VALUE, mMinValue);
+ b.putFloat(KEY_MAX_VALUE, mMaxValue);
+ b.putFloat(KEY_CURRENT_VALUE, mCurrentValue);
+ b.putFloat(KEY_STEP_VALUE, mStepValue);
+ b.putCharSequence(KEY_FORMAT_STRING, mFormatString);
+ return b;
+ }
+
+ /**
+ * Validate constructor parameters
+ *
+ * @throws IllegalArgumentException if the parameters passed do not make a valid range
+ */
+ private void validate() {
+ if (Float.compare(mMinValue, mMaxValue) > 0) {
+ throw new IllegalArgumentException(
+ String.format("minValue=%f > maxValue=%f", mMinValue, mMaxValue));
+ }
+ if (Float.compare(mMinValue, mCurrentValue) > 0) {
+ throw new IllegalArgumentException(
+ String.format("minValue=%f > currentValue=%f", mMinValue, mCurrentValue));
+ }
+ if (Float.compare(mCurrentValue, mMaxValue) > 0) {
+ throw new IllegalArgumentException(
+ String.format("currentValue=%f > maxValue=%f", mCurrentValue, mMaxValue));
+ }
+ if (mStepValue <= 0) {
+ throw new IllegalArgumentException(String.format("stepValue=%f <= 0", mStepValue));
+ }
+ }
+}
diff --git a/android/service/controls/templates/StatelessTemplate.java b/android/service/controls/templates/StatelessTemplate.java
new file mode 100644
index 0000000..c052412
--- /dev/null
+++ b/android/service/controls/templates/StatelessTemplate.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 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.controls.templates;
+
+import android.annotation.NonNull;
+import android.os.Bundle;
+import android.service.controls.Control;
+import android.service.controls.actions.CommandAction;
+
+/**
+ * A template for a {@link Control} which has no state.
+ *
+ * @see CommandAction
+ */
+public final class StatelessTemplate extends ControlTemplate {
+
+ /**
+ * @return {@link ControlTemplate#TYPE_STATELESS}
+ */
+ @Override
+ public int getTemplateType() {
+ return TYPE_STATELESS;
+ }
+
+ /**
+ * Construct a new {@link StatelessTemplate} from a {@link Bundle}
+ * @hide
+ */
+ StatelessTemplate(@NonNull Bundle b) {
+ super(b);
+ }
+
+ /**
+ * Construct a new {@link StatelessTemplate}
+ * @param templateId the identifier for this template
+ */
+ public StatelessTemplate(@NonNull String templateId) {
+ super(templateId);
+ }
+}
diff --git a/android/service/controls/templates/TemperatureControlTemplate.java b/android/service/controls/templates/TemperatureControlTemplate.java
new file mode 100644
index 0000000..96be97a
--- /dev/null
+++ b/android/service/controls/templates/TemperatureControlTemplate.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2019 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.controls.templates;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Bundle;
+import android.service.controls.Control;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A template for a temperature related {@link Control} that supports multiple modes.
+ *
+ * Both the current mode and the active mode for the control can be specified. The combination of
+ * the {@link Control#getDeviceType} and the current and active mode will determine colors and
+ * transitions for the UI element.
+ */
+public final class TemperatureControlTemplate extends ControlTemplate {
+
+ private static final String TAG = "ThermostatTemplate";
+
+ private static final @TemplateType int TYPE = TYPE_TEMPERATURE;
+ private static final String KEY_TEMPLATE = "key_template";
+ private static final String KEY_CURRENT_MODE = "key_current_mode";
+ private static final String KEY_CURRENT_ACTIVE_MODE = "key_current_active_mode";
+ private static final String KEY_MODES = "key_modes";
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ MODE_UNKNOWN,
+ MODE_OFF,
+ MODE_HEAT,
+ MODE_COOL,
+ MODE_HEAT_COOL,
+ MODE_ECO
+ })
+ public @interface Mode {}
+
+ private static final int NUM_MODES = 6;
+
+ /**
+ * Use when the current or active mode of the device is not known
+ */
+ public static final @Mode int MODE_UNKNOWN = 0;
+
+ /**
+ * Indicates that the current or active mode of the device is off.
+ */
+ public static final @Mode int MODE_OFF = 1;
+
+ /**
+ * Indicates that the current or active mode of the device is set to heat.
+ */
+ public static final @Mode int MODE_HEAT = 2;
+
+ /**
+ * Indicates that the current or active mode of the device is set to cool.
+ */
+ public static final @Mode int MODE_COOL = 3;
+
+ /**
+ * Indicates that the current or active mode of the device is set to heat-cool.
+ */
+ public static final @Mode int MODE_HEAT_COOL = 4;
+
+ /**
+ * Indicates that the current or active mode of the device is set to eco.
+ */
+ public static final @Mode int MODE_ECO = 5;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = {
+ FLAG_MODE_OFF,
+ FLAG_MODE_HEAT,
+ FLAG_MODE_COOL,
+ FLAG_MODE_HEAT_COOL,
+ FLAG_MODE_ECO
+ })
+ public @interface ModeFlag {}
+
+ /**
+ * Flag to indicate that the device supports off mode.
+ */
+ public static final int FLAG_MODE_OFF = 1 << MODE_OFF;
+
+ /**
+ * Flag to indicate that the device supports heat mode.
+ */
+ public static final int FLAG_MODE_HEAT = 1 << MODE_HEAT;
+
+ /**
+ * Flag to indicate that the device supports cool mode.
+ */
+ public static final int FLAG_MODE_COOL = 1 << MODE_COOL;
+
+ /**
+ * Flag to indicate that the device supports heat-cool mode.
+ */
+ public static final int FLAG_MODE_HEAT_COOL = 1 << MODE_HEAT_COOL;
+
+ /**
+ * Flag to indicate that the device supports eco mode.
+ */
+ public static final int FLAG_MODE_ECO = 1 << MODE_ECO;
+ private static final int ALL_FLAGS =
+ FLAG_MODE_OFF |
+ FLAG_MODE_HEAT |
+ FLAG_MODE_COOL |
+ FLAG_MODE_HEAT_COOL |
+ FLAG_MODE_ECO;
+
+ private static final int[] modeToFlag = new int[]{
+ 0,
+ FLAG_MODE_OFF,
+ FLAG_MODE_HEAT,
+ FLAG_MODE_COOL,
+ FLAG_MODE_HEAT_COOL,
+ FLAG_MODE_ECO
+ };
+
+ private final @NonNull ControlTemplate mTemplate;
+ private final @Mode int mCurrentMode;
+ private final @Mode int mCurrentActiveMode;
+ private final @ModeFlag int mModes;
+
+ /**
+ * Construct a new {@link TemperatureControlTemplate}.
+ *
+ * The current and active mode have to be among the ones supported by the flags.
+ *
+ * @param templateId the identifier for this template object
+ * @param controlTemplate a template to use for interaction with the user
+ * @param currentMode the current mode for the {@link Control}
+ * @param currentActiveMode the current active mode for the {@link Control}
+ * @param modesFlag a flag representing the available modes for the {@link Control}
+ * @throws IllegalArgumentException if the parameters passed do not make a valid template.
+ */
+ public TemperatureControlTemplate(@NonNull String templateId,
+ @NonNull ControlTemplate controlTemplate,
+ @Mode int currentMode,
+ @Mode int currentActiveMode,
+ @ModeFlag int modesFlag) {
+ super(templateId);
+ Preconditions.checkNotNull(controlTemplate);
+ mTemplate = controlTemplate;
+
+ if (currentMode < 0 || currentMode >= NUM_MODES) {
+ Log.e(TAG, "Invalid current mode:" + currentMode);
+ mCurrentMode = MODE_UNKNOWN;
+ } else {
+ mCurrentMode = currentMode;
+ }
+
+ if (currentActiveMode < 0 || currentActiveMode >= NUM_MODES) {
+ Log.e(TAG, "Invalid current active mode:" + currentActiveMode);
+ mCurrentActiveMode = MODE_UNKNOWN;
+ } else {
+ mCurrentActiveMode = currentActiveMode;
+ }
+
+ mModes = modesFlag & ALL_FLAGS;
+ if (mCurrentMode != MODE_UNKNOWN && (modeToFlag[mCurrentMode] & mModes) == 0) {
+ throw new IllegalArgumentException("Mode " + mCurrentMode + " not supported in flag.");
+ }
+ if (mCurrentActiveMode != MODE_UNKNOWN && (modeToFlag[mCurrentActiveMode] & mModes) == 0) {
+ throw new IllegalArgumentException(
+ "Mode " + currentActiveMode + " not supported in flag.");
+ }
+ }
+
+ /**
+ * @param b
+ * @hide
+ */
+ TemperatureControlTemplate(@NonNull Bundle b) {
+ super(b);
+ mTemplate = ControlTemplate.createTemplateFromBundle(b.getBundle(KEY_TEMPLATE));
+ mCurrentMode = b.getInt(KEY_CURRENT_MODE);
+ mCurrentActiveMode = b.getInt(KEY_CURRENT_ACTIVE_MODE);
+ mModes = b.getInt(KEY_MODES);
+ }
+
+ /**
+ * @return
+ * @hide
+ */
+ @Override
+ @NonNull
+ Bundle getDataBundle() {
+ Bundle b = super.getDataBundle();
+ b.putBundle(KEY_TEMPLATE, mTemplate.getDataBundle());
+ b.putInt(KEY_CURRENT_MODE, mCurrentMode);
+ b.putInt(KEY_CURRENT_ACTIVE_MODE, mCurrentActiveMode);
+ b.putInt(KEY_MODES, mModes);
+ return b;
+ }
+
+ @NonNull
+ public ControlTemplate getTemplate() {
+ return mTemplate;
+ }
+
+ public int getCurrentMode() {
+ return mCurrentMode;
+ }
+
+ public int getCurrentActiveMode() {
+ return mCurrentActiveMode;
+ }
+
+ public int getModes() {
+ return mModes;
+ }
+
+ /**
+ * @return {@link ControlTemplate#TYPE_TEMPERATURE}
+ */
+ @Override
+ public int getTemplateType() {
+ return TYPE;
+ }
+}
diff --git a/android/service/controls/templates/ToggleRangeTemplate.java b/android/service/controls/templates/ToggleRangeTemplate.java
new file mode 100644
index 0000000..cd6a2fc
--- /dev/null
+++ b/android/service/controls/templates/ToggleRangeTemplate.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 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.controls.templates;
+
+import android.annotation.NonNull;
+import android.os.Bundle;
+import android.service.controls.Control;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * A template for a {@link Control} supporting toggling and a range.
+ *
+ * @see ToggleTemplate
+ * @see RangeTemplate
+ */
+public final class ToggleRangeTemplate extends ControlTemplate {
+
+ private static final @TemplateType int TYPE = TYPE_TOGGLE_RANGE;
+ private static final String KEY_BUTTON = "key_button";
+ private static final String KEY_RANGE = "key_range";
+
+ private @NonNull final ControlButton mControlButton;
+ private @NonNull final RangeTemplate mRangeTemplate;
+
+ /**
+ * @param b
+ * @hide
+ */
+ ToggleRangeTemplate(@NonNull Bundle b) {
+ super(b);
+ mControlButton = b.getParcelable(KEY_BUTTON);
+ mRangeTemplate = new RangeTemplate(b.getBundle(KEY_RANGE));
+ }
+
+ /**
+ * Constructs a new {@link ToggleRangeTemplate}.
+ * @param templateId the identifier for this template.
+ * @param button a {@link ControlButton} to use for the toggle interface
+ * @param range a {@link RangeTemplate} to use for the range interface
+ */
+ public ToggleRangeTemplate(@NonNull String templateId,
+ @NonNull ControlButton button,
+ @NonNull RangeTemplate range) {
+ super(templateId);
+ Preconditions.checkNotNull(button);
+ Preconditions.checkNotNull(range);
+ mControlButton = button;
+ mRangeTemplate = range;
+ }
+
+ /**
+ * Constructs a new {@link ToggleRangeTemplate}.
+ * @param templateId the identifier for this template.
+ * @param checked true if the toggle should be rendered as active.
+ * @param actionDescription action description for the button.
+ * @param range {@link RangeTemplate} to use for the range interface
+ * @see ControlButton
+ */
+ public ToggleRangeTemplate(@NonNull String templateId,
+ boolean checked,
+ @NonNull CharSequence actionDescription,
+ @NonNull RangeTemplate range) {
+ this(templateId,
+ new ControlButton(checked, actionDescription),
+ range);
+ }
+
+ /**
+ * @return
+ * @hide
+ */
+ @Override
+ @NonNull
+ Bundle getDataBundle() {
+ Bundle b = super.getDataBundle();
+ b.putParcelable(KEY_BUTTON, mControlButton);
+ b.putBundle(KEY_RANGE, mRangeTemplate.getDataBundle());
+ return b;
+ }
+
+ @NonNull
+ public RangeTemplate getRange() {
+ return mRangeTemplate;
+ }
+
+ public boolean isChecked() {
+ return mControlButton.isChecked();
+ }
+
+ @NonNull
+ public CharSequence getActionDescription() {
+ return mControlButton.getActionDescription();
+ }
+
+ /**
+ * @return {@link ControlTemplate#TYPE_TOGGLE_RANGE}
+ */
+ @Override
+ public int getTemplateType() {
+ return TYPE;
+ }
+}
diff --git a/android/service/controls/templates/ToggleTemplate.java b/android/service/controls/templates/ToggleTemplate.java
new file mode 100644
index 0000000..e4aa6b0
--- /dev/null
+++ b/android/service/controls/templates/ToggleTemplate.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2019 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.controls.templates;
+
+import android.annotation.NonNull;
+import android.os.Bundle;
+import android.service.controls.Control;
+import android.service.controls.actions.BooleanAction;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * A template for a {@link Control} with a single button that can be toggled between two states.
+ *
+ * The states for the toggle correspond to the states in {@link ControlButton#isChecked()}.
+ * An action on this template will originate a {@link BooleanAction} to change that state.
+ *
+ * @see BooleanAction
+ */
+public final class ToggleTemplate extends ControlTemplate {
+
+ private static final @TemplateType int TYPE = TYPE_TOGGLE;
+ private static final String KEY_BUTTON = "key_button";
+ private final @NonNull ControlButton mButton;
+
+ /**
+ * @param templateId the identifier for this template object
+ * @param button a {@link ControlButton} that can show the current state and toggle it
+ */
+ public ToggleTemplate(@NonNull String templateId, @NonNull ControlButton button) {
+ super(templateId);
+ Preconditions.checkNotNull(button);
+ mButton = button;
+ }
+
+ /**
+ * @param b
+ * @hide
+ */
+ ToggleTemplate(Bundle b) {
+ super(b);
+ mButton = b.getParcelable(KEY_BUTTON);
+ }
+
+ public boolean isChecked() {
+ return mButton.isChecked();
+ }
+
+ @NonNull
+ public CharSequence getContentDescription() {
+ return mButton.getActionDescription();
+ }
+
+ /**
+ * @return {@link ControlTemplate#TYPE_TOGGLE}
+ */
+ @Override
+ public int getTemplateType() {
+ return TYPE;
+ }
+
+ /**
+ * @return
+ * @hide
+ */
+ @Override
+ @NonNull
+ Bundle getDataBundle() {
+ Bundle b = super.getDataBundle();
+ b.putParcelable(KEY_BUTTON, mButton);
+ return b;
+ }
+}
diff --git a/android/service/dataloader/DataLoaderService.java b/android/service/dataloader/DataLoaderService.java
new file mode 100644
index 0000000..e35b8b7
--- /dev/null
+++ b/android/service/dataloader/DataLoaderService.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2019 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.dataloader;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.DataLoaderParams;
+import android.content.pm.DataLoaderParamsParcel;
+import android.content.pm.FileSystemControlParcel;
+import android.content.pm.IDataLoader;
+import android.content.pm.IDataLoaderStatusListener;
+import android.content.pm.InstallationFile;
+import android.content.pm.InstallationFileParcel;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.util.ExceptionUtils;
+import android.util.Slog;
+
+import libcore.io.IoUtils;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * The base class for implementing data loader service to control data loaders. Expecting
+ * Incremental Service to bind to a children class of this.
+ *
+ * WARNING: This is a system API to aid internal development.
+ * Use at your own risk. It will change or be removed without warning.
+ *
+ * TODO(b/136132412): update with latest API design
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class DataLoaderService extends Service {
+ private static final String TAG = "DataLoaderService";
+ private final DataLoaderBinderService mBinder = new DataLoaderBinderService();
+
+ /**
+ * Managed DataLoader interface. Each instance corresponds to a single installation session.
+ * @hide
+ */
+ @SystemApi
+ public interface DataLoader {
+ /**
+ * A virtual constructor.
+ *
+ * @param dataLoaderParams parameters set in the installation session
+ * @param connector FS API wrapper
+ * @return True if initialization of a Data Loader was successful. False will be reported to
+ * PackageManager and fail the installation
+ */
+ boolean onCreate(@NonNull DataLoaderParams dataLoaderParams,
+ @NonNull FileSystemConnector connector);
+
+ /**
+ * Prepare installation image. After this method succeeds installer will validate the files
+ * and continue installation.
+ *
+ * @param addedFiles list of files created in this installation session.
+ * @param removedFiles list of files removed in this installation session.
+ * @return false if unable to create and populate all addedFiles.
+ */
+ boolean onPrepareImage(@NonNull Collection<InstallationFile> addedFiles,
+ @NonNull Collection<String> removedFiles);
+ }
+
+ /**
+ * DataLoader factory method.
+ *
+ * @return An instance of a DataLoader.
+ * @hide
+ */
+ @SystemApi
+ public @Nullable DataLoader onCreateDataLoader(@NonNull DataLoaderParams dataLoaderParams) {
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ public final @NonNull IBinder onBind(@NonNull Intent intent) {
+ return (IBinder) mBinder;
+ }
+
+ private class DataLoaderBinderService extends IDataLoader.Stub {
+ @Override
+ public void create(int id, @NonNull DataLoaderParamsParcel params,
+ @NonNull FileSystemControlParcel control,
+ @NonNull IDataLoaderStatusListener listener)
+ throws RuntimeException {
+ try {
+ nativeCreateDataLoader(id, control, params, listener);
+ } catch (Exception ex) {
+ Slog.e(TAG, "Failed to create native loader for " + id, ex);
+ destroy(id);
+ throw new RuntimeException(ex);
+ } finally {
+ if (control.incremental != null) {
+ IoUtils.closeQuietly(control.incremental.cmd);
+ IoUtils.closeQuietly(control.incremental.pendingReads);
+ IoUtils.closeQuietly(control.incremental.log);
+ }
+ }
+ }
+
+ @Override
+ public void start(int id) {
+ if (!nativeStartDataLoader(id)) {
+ Slog.e(TAG, "Failed to start loader: " + id);
+ }
+ }
+
+ @Override
+ public void stop(int id) {
+ if (!nativeStopDataLoader(id)) {
+ Slog.w(TAG, "Failed to stop loader: " + id);
+ }
+ }
+
+ @Override
+ public void destroy(int id) {
+ if (!nativeDestroyDataLoader(id)) {
+ Slog.w(TAG, "Failed to destroy loader: " + id);
+ }
+ }
+
+ @Override
+ public void prepareImage(int id, InstallationFileParcel[] addedFiles,
+ String[] removedFiles) {
+ if (!nativePrepareImage(id, addedFiles, removedFiles)) {
+ Slog.w(TAG, "Failed to prepare image for data loader: " + id);
+ }
+ }
+ }
+
+ /**
+ * Used by the DataLoaderService implementations.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final class FileSystemConnector {
+ /**
+ * Create a wrapper for a native instance.
+ *
+ * @hide
+ */
+ FileSystemConnector(long nativeInstance) {
+ mNativeInstance = nativeInstance;
+ }
+
+ /**
+ * Write data to an installation file from an arbitrary FD.
+ *
+ * @param name name of file previously added to the installation session.
+ * @param offsetBytes offset into the file to begin writing at, or 0 to start at the
+ * beginning of the file.
+ * @param lengthBytes total size of the file being written, used to preallocate the
+ * underlying disk space, or -1 if unknown. The system may clear various
+ * caches as needed to allocate this space.
+ * @param incomingFd FD to read bytes from.
+ * @throws IOException if trouble opening the file for writing, such as lack of disk space
+ * or unavailable media.
+ */
+ @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES)
+ public void writeData(@NonNull String name, long offsetBytes, long lengthBytes,
+ @NonNull ParcelFileDescriptor incomingFd) throws IOException {
+ try {
+ nativeWriteData(mNativeInstance, name, offsetBytes, lengthBytes, incomingFd);
+ } catch (RuntimeException e) {
+ ExceptionUtils.maybeUnwrapIOException(e);
+ throw e;
+ }
+ }
+
+ private final long mNativeInstance;
+ }
+
+ /* Native methods */
+ private native boolean nativeCreateDataLoader(int storageId,
+ @NonNull FileSystemControlParcel control,
+ @NonNull DataLoaderParamsParcel params,
+ IDataLoaderStatusListener listener);
+
+ private native boolean nativeStartDataLoader(int storageId);
+
+ private native boolean nativeStopDataLoader(int storageId);
+
+ private native boolean nativeDestroyDataLoader(int storageId);
+
+ private native boolean nativePrepareImage(int storageId,
+ InstallationFileParcel[] addedFiles, String[] removedFiles);
+
+ private static native void nativeWriteData(long nativeInstance, String name, long offsetBytes,
+ long lengthBytes, ParcelFileDescriptor incomingFd);
+
+}
diff --git a/android/service/dreams/DreamActivity.java b/android/service/dreams/DreamActivity.java
new file mode 100644
index 0000000..0a29edc
--- /dev/null
+++ b/android/service/dreams/DreamActivity.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 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.dreams;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.WindowInsets;
+
+/**
+ * The Activity used by the {@link DreamService} to draw screensaver content
+ * on the screen. This activity runs in dream application's process, but is started by a
+ * specialized method: {@link com.android.server.wm.ActivityTaskManagerService#startDreamActivity}.
+ * Hence, it does not have to be declared in the dream application's manifest.
+ *
+ * We use an activity as the dream canvas, because it interacts easier with other activities on
+ * the screen (compared to a hover window). However, the DreamService is in charge of the dream and
+ * it receives all Window.Callbacks from its main window. Since a window can have only one callback
+ * receiver, the activity will not receive any window callbacks.
+ *
+ * Prior to the DreamActivity, the DreamService used to work with a hovering window and give the
+ * screensaver application control over that window. The DreamActivity is a replacement to that
+ * hover window. Using an activity allows for better-defined interactions with the rest of the
+ * activities on screen. The switch to DreamActivity should be transparent to the screensaver
+ * application, i.e. the application will still use DreamService APIs and not notice that the
+ * system is using an activity behind the scenes.
+ *
+ * @hide
+ */
+public class DreamActivity extends Activity {
+ static final String EXTRA_CALLBACK = "binder";
+
+ public DreamActivity() {}
+
+ @Override
+ public void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+
+ DreamService.DreamServiceWrapper callback =
+ (DreamService.DreamServiceWrapper) getIntent().getIBinderExtra(EXTRA_CALLBACK);
+
+ if (callback != null) {
+ callback.onActivityCreated(this);
+ }
+
+ // Hide all insets (nav bar, status bar, etc) when the dream is showing
+ getWindow().getInsetsController().hide(WindowInsets.Type.systemBars());
+ }
+}
diff --git a/android/service/dreams/DreamManagerInternal.java b/android/service/dreams/DreamManagerInternal.java
new file mode 100644
index 0000000..7bf5c38
--- /dev/null
+++ b/android/service/dreams/DreamManagerInternal.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014 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.dreams;
+
+import android.content.ComponentName;
+
+/**
+ * Dream manager local system service interface.
+ *
+ * @hide Only for use within the system server.
+ */
+public abstract class DreamManagerInternal {
+ /**
+ * Called by the power manager to start a dream.
+ *
+ * @param doze If true, starts the doze dream component if one has been configured,
+ * otherwise starts the user-specified dream.
+ */
+ public abstract void startDream(boolean doze);
+
+ /**
+ * Called by the power manager to stop a dream.
+ *
+ * @param immediate If true, ends the dream summarily, otherwise gives it some time
+ * to perform a proper exit transition.
+ */
+ public abstract void stopDream(boolean immediate);
+
+ /**
+ * Called by the power manager to determine whether a dream is running.
+ */
+ public abstract boolean isDreaming();
+
+ /**
+ * Called by the ActivityTaskManagerService to verify that the startDreamActivity
+ * request comes from the current active dream component.
+ *
+ * This function and its call path should not acquire the DreamManagerService lock
+ * to avoid deadlock with the ActivityTaskManager lock.
+ *
+ * TODO: Make this interaction push-based - the DreamManager should inform the
+ * ActivityTaskManager whenever the active dream component changes.
+ *
+ * @param doze If true returns the current active doze component. Otherwise, returns the
+ * active dream component.
+ */
+ public abstract ComponentName getActiveDreamComponent(boolean doze);
+}
diff --git a/android/service/dreams/DreamService.java b/android/service/dreams/DreamService.java
new file mode 100644
index 0000000..337027e
--- /dev/null
+++ b/android/service/dreams/DreamService.java
@@ -0,0 +1,1167 @@
+/**
+ * Copyright (C) 2012 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.dreams;
+
+import android.annotation.IdRes;
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.app.Activity;
+import android.app.ActivityTaskManager;
+import android.app.AlarmManager;
+import android.app.Service;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.Slog;
+import android.view.ActionMode;
+import android.view.Display;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.SearchEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.DumpUtils.Dump;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Extend this class to implement a custom dream (available to the user as a "Daydream").
+ *
+ * <p>Dreams are interactive screensavers launched when a charging device is idle, or docked in a
+ * desk dock. Dreams provide another modality for apps to express themselves, tailored for
+ * an exhibition/lean-back experience.</p>
+ *
+ * <p>The {@code DreamService} lifecycle is as follows:</p>
+ * <ol>
+ * <li>{@link #onAttachedToWindow}
+ * <p>Use this for initial setup, such as calling {@link #setContentView setContentView()}.</li>
+ * <li>{@link #onDreamingStarted}
+ * <p>Your dream has started, so you should begin animations or other behaviors here.</li>
+ * <li>{@link #onDreamingStopped}
+ * <p>Use this to stop the things you started in {@link #onDreamingStarted}.</li>
+ * <li>{@link #onDetachedFromWindow}
+ * <p>Use this to dismantle resources (for example, detach from handlers
+ * and listeners).</li>
+ * </ol>
+ *
+ * <p>In addition, onCreate and onDestroy (from the Service interface) will also be called, but
+ * initialization and teardown should be done by overriding the hooks above.</p>
+ *
+ * <p>To be available to the system, your {@code DreamService} should be declared in the
+ * manifest as follows:</p>
+ * <pre>
+ * <service
+ * android:name=".MyDream"
+ * android:exported="true"
+ * android:icon="@drawable/my_icon"
+ * android:label="@string/my_dream_label" >
+ *
+ * <intent-filter>
+ * <action android:name="android.service.dreams.DreamService" />
+ * <category android:name="android.intent.category.DEFAULT" />
+ * </intent-filter>
+ *
+ * <!-- Point to additional information for this dream (optional) -->
+ * <meta-data
+ * android:name="android.service.dream"
+ * android:resource="@xml/my_dream" />
+ * </service>
+ * </pre>
+ *
+ * <p>If specified with the {@code <meta-data>} element,
+ * additional information for the dream is defined using the
+ * {@link android.R.styleable#Dream <dream>} element in a separate XML file.
+ * Currently, the only addtional
+ * information you can provide is for a settings activity that allows the user to configure
+ * the dream behavior. For example:</p>
+ * <p class="code-caption">res/xml/my_dream.xml</p>
+ * <pre>
+ * <dream xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:settingsActivity="com.example.app/.MyDreamSettingsActivity" />
+ * </pre>
+ * <p>This makes a Settings button available alongside your dream's listing in the
+ * system settings, which when pressed opens the specified activity.</p>
+ *
+ *
+ * <p>To specify your dream layout, call {@link #setContentView}, typically during the
+ * {@link #onAttachedToWindow} callback. For example:</p>
+ * <pre>
+ * public class MyDream extends DreamService {
+ *
+ * @Override
+ * public void onAttachedToWindow() {
+ * super.onAttachedToWindow();
+ *
+ * // Exit dream upon user touch
+ * setInteractive(false);
+ * // Hide system UI
+ * setFullscreen(true);
+ * // Set the dream layout
+ * setContentView(R.layout.dream);
+ * }
+ * }
+ * </pre>
+ *
+ * <p>When targeting api level 21 and above, you must declare the service in your manifest file
+ * with the {@link android.Manifest.permission#BIND_DREAM_SERVICE} permission. For example:</p>
+ * <pre>
+ * <service
+ * android:name=".MyDream"
+ * android:exported="true"
+ * android:icon="@drawable/my_icon"
+ * android:label="@string/my_dream_label"
+ * android:permission="android.permission.BIND_DREAM_SERVICE">
+ * <intent-filter>
+ * <action android:name=”android.service.dreams.DreamService” />
+ * <category android:name=”android.intent.category.DEFAULT” />
+ * </intent-filter>
+ * </service>
+ * </pre>
+ */
+public class DreamService extends Service implements Window.Callback {
+ private final String TAG = DreamService.class.getSimpleName() + "[" + getClass().getSimpleName() + "]";
+
+ /**
+ * The name of the dream manager service.
+ * @hide
+ */
+ public static final String DREAM_SERVICE = "dreams";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE =
+ "android.service.dreams.DreamService";
+
+ /**
+ * Name under which a Dream publishes information about itself.
+ * This meta-data must reference an XML resource containing
+ * a <code><{@link android.R.styleable#Dream dream}></code>
+ * tag.
+ */
+ public static final String DREAM_META_DATA = "android.service.dream";
+
+ private final IDreamManager mDreamManager;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private IBinder mDreamToken;
+ private Window mWindow;
+ private Activity mActivity;
+ private boolean mInteractive;
+ private boolean mFullscreen;
+ private boolean mScreenBright = true;
+ private boolean mStarted;
+ private boolean mWaking;
+ private boolean mFinished;
+ private boolean mCanDoze;
+ private boolean mDozing;
+ private boolean mWindowless;
+ private int mDozeScreenState = Display.STATE_UNKNOWN;
+ private int mDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
+
+ private boolean mDebug = false;
+
+ private DreamServiceWrapper mDreamServiceWrapper;
+ private Runnable mDispatchAfterOnAttachedToWindow;
+
+ public DreamService() {
+ mDreamManager = IDreamManager.Stub.asInterface(ServiceManager.getService(DREAM_SERVICE));
+ }
+
+ /**
+ * @hide
+ */
+ public void setDebug(boolean dbg) {
+ mDebug = dbg;
+ }
+
+ // begin Window.Callback methods
+ /** {@inheritDoc} */
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ // TODO: create more flexible version of mInteractive that allows use of KEYCODE_BACK
+ if (!mInteractive) {
+ if (mDebug) Slog.v(TAG, "Waking up on keyEvent");
+ wakeUp();
+ return true;
+ } else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ if (mDebug) Slog.v(TAG, "Waking up on back key");
+ wakeUp();
+ return true;
+ }
+ return mWindow.superDispatchKeyEvent(event);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+ if (!mInteractive) {
+ if (mDebug) Slog.v(TAG, "Waking up on keyShortcutEvent");
+ wakeUp();
+ return true;
+ }
+ return mWindow.superDispatchKeyShortcutEvent(event);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ // TODO: create more flexible version of mInteractive that allows clicks
+ // but finish()es on any other kind of activity
+ if (!mInteractive) {
+ if (mDebug) Slog.v(TAG, "Waking up on touchEvent");
+ wakeUp();
+ return true;
+ }
+ return mWindow.superDispatchTouchEvent(event);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean dispatchTrackballEvent(MotionEvent event) {
+ if (!mInteractive) {
+ if (mDebug) Slog.v(TAG, "Waking up on trackballEvent");
+ wakeUp();
+ return true;
+ }
+ return mWindow.superDispatchTrackballEvent(event);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean dispatchGenericMotionEvent(MotionEvent event) {
+ if (!mInteractive) {
+ if (mDebug) Slog.v(TAG, "Waking up on genericMotionEvent");
+ wakeUp();
+ return true;
+ }
+ return mWindow.superDispatchGenericMotionEvent(event);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public View onCreatePanelView(int featureId) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onCreatePanelMenu(int featureId, Menu menu) {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onPreparePanel(int featureId, View view, Menu menu) {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onWindowAttributesChanged(LayoutParams attrs) {
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onContentChanged() {
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onAttachedToWindow() {
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onDetachedFromWindow() {
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onPanelClosed(int featureId, Menu menu) {
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onSearchRequested(SearchEvent event) {
+ return onSearchRequested();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onSearchRequested() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback callback) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ActionMode onWindowStartingActionMode(
+ android.view.ActionMode.Callback callback, int type) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onActionModeFinished(ActionMode mode) {
+ }
+ // end Window.Callback methods
+
+ // begin public api
+ /**
+ * Retrieves the current {@link android.view.WindowManager} for the dream.
+ * Behaves similarly to {@link android.app.Activity#getWindowManager()}.
+ *
+ * @return The current window manager, or null if the dream is not started.
+ */
+ public WindowManager getWindowManager() {
+ return mWindow != null ? mWindow.getWindowManager() : null;
+ }
+
+ /**
+ * Retrieves the current {@link android.view.Window} for the dream.
+ * Behaves similarly to {@link android.app.Activity#getWindow()}.
+ *
+ * @return The current window, or null if the dream is not started.
+ */
+ public Window getWindow() {
+ return mWindow;
+ }
+
+ /**
+ * Inflates a layout resource and set it to be the content view for this Dream.
+ * Behaves similarly to {@link android.app.Activity#setContentView(int)}.
+ *
+ * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
+ *
+ * @param layoutResID Resource ID to be inflated.
+ *
+ * @see #setContentView(android.view.View)
+ * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
+ */
+ public void setContentView(@LayoutRes int layoutResID) {
+ getWindow().setContentView(layoutResID);
+ }
+
+ /**
+ * Sets a view to be the content view for this Dream.
+ * Behaves similarly to {@link android.app.Activity#setContentView(android.view.View)} in an activity,
+ * including using {@link ViewGroup.LayoutParams#MATCH_PARENT} as the layout height and width of the view.
+ *
+ * <p>Note: This requires a window, so you should usually call it during
+ * {@link #onAttachedToWindow()} and never earlier (you <strong>cannot</strong> call it
+ * during {@link #onCreate}).</p>
+ *
+ * @see #setContentView(int)
+ * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
+ */
+ public void setContentView(View view) {
+ getWindow().setContentView(view);
+ }
+
+ /**
+ * Sets a view to be the content view for this Dream.
+ * Behaves similarly to
+ * {@link android.app.Activity#setContentView(android.view.View, android.view.ViewGroup.LayoutParams)}
+ * in an activity.
+ *
+ * <p>Note: This requires a window, so you should usually call it during
+ * {@link #onAttachedToWindow()} and never earlier (you <strong>cannot</strong> call it
+ * during {@link #onCreate}).</p>
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ *
+ * @see #setContentView(android.view.View)
+ * @see #setContentView(int)
+ */
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ getWindow().setContentView(view, params);
+ }
+
+ /**
+ * Adds a view to the Dream's window, leaving other content views in place.
+ *
+ * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
+ *
+ * @param view The desired content to display.
+ * @param params Layout parameters for the view.
+ */
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ getWindow().addContentView(view, params);
+ }
+
+ /**
+ * Finds a view that was identified by the id attribute from the XML that
+ * was processed in {@link #onCreate}.
+ *
+ * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return The view if found or null otherwise.
+ * @see View#findViewById(int)
+ * @see DreamService#requireViewById(int)
+ */
+ @Nullable
+ public <T extends View> T findViewById(@IdRes int id) {
+ return getWindow().findViewById(id);
+ }
+
+ /**
+ * Finds a view that was identified by the id attribute from the XML that was processed in
+ * {@link #onCreate}, or throws an IllegalArgumentException if the ID is invalid or there is no
+ * matching view in the hierarchy.
+ *
+ * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#requireViewById(int)
+ * @see DreamService#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException(
+ "ID does not reference a View inside this DreamService");
+ }
+ return view;
+ }
+
+ /**
+ * Marks this dream as interactive to receive input events.
+ *
+ * <p>Non-interactive dreams (default) will dismiss on the first input event.</p>
+ *
+ * <p>Interactive dreams should call {@link #finish()} to dismiss themselves.</p>
+ *
+ * @param interactive True if this dream will handle input events.
+ */
+ public void setInteractive(boolean interactive) {
+ mInteractive = interactive;
+ }
+
+ /**
+ * Returns whether or not this dream is interactive. Defaults to false.
+ *
+ * @see #setInteractive(boolean)
+ */
+ public boolean isInteractive() {
+ return mInteractive;
+ }
+
+ /**
+ * Controls {@link android.view.WindowManager.LayoutParams#FLAG_FULLSCREEN}
+ * on the dream's window.
+ *
+ * @param fullscreen If true, the fullscreen flag will be set; else it
+ * will be cleared.
+ */
+ public void setFullscreen(boolean fullscreen) {
+ if (mFullscreen != fullscreen) {
+ mFullscreen = fullscreen;
+ int flag = WindowManager.LayoutParams.FLAG_FULLSCREEN;
+ applyWindowFlags(mFullscreen ? flag : 0, flag);
+ }
+ }
+
+ /**
+ * Returns whether or not this dream is in fullscreen mode. Defaults to false.
+ *
+ * @see #setFullscreen(boolean)
+ */
+ public boolean isFullscreen() {
+ return mFullscreen;
+ }
+
+ /**
+ * Marks this dream as keeping the screen bright while dreaming.
+ *
+ * @param screenBright True to keep the screen bright while dreaming.
+ */
+ public void setScreenBright(boolean screenBright) {
+ if (mScreenBright != screenBright) {
+ mScreenBright = screenBright;
+ int flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+ applyWindowFlags(mScreenBright ? flag : 0, flag);
+ }
+ }
+
+ /**
+ * Returns whether or not this dream keeps the screen bright while dreaming.
+ * Defaults to false, allowing the screen to dim if necessary.
+ *
+ * @see #setScreenBright(boolean)
+ */
+ public boolean isScreenBright() {
+ return getWindowFlagValue(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, mScreenBright);
+ }
+
+ /**
+ * Marks this dream as windowless. Only available to doze dreams.
+ *
+ * @hide
+ *
+ */
+ public void setWindowless(boolean windowless) {
+ mWindowless = windowless;
+ }
+
+ /**
+ * Returns whether or not this dream is windowless. Only available to doze dreams.
+ *
+ * @hide
+ */
+ public boolean isWindowless() {
+ return mWindowless;
+ }
+
+ /**
+ * Returns true if this dream is allowed to doze.
+ * <p>
+ * The value returned by this method is only meaningful when the dream has started.
+ * </p>
+ *
+ * @return True if this dream can doze.
+ * @see #startDozing
+ * @hide For use by system UI components only.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public boolean canDoze() {
+ return mCanDoze;
+ }
+
+ /**
+ * Starts dozing, entering a deep dreamy sleep.
+ * <p>
+ * Dozing enables the system to conserve power while the user is not actively interacting
+ * with the device. While dozing, the display will remain on in a low-power state
+ * and will continue to show its previous contents but the application processor and
+ * other system components will be allowed to suspend when possible.
+ * </p><p>
+ * While the application processor is suspended, the dream may stop executing code
+ * for long periods of time. Prior to being suspended, the dream may schedule periodic
+ * wake-ups to render new content by scheduling an alarm with the {@link AlarmManager}.
+ * The dream may also keep the CPU awake by acquiring a
+ * {@link android.os.PowerManager#PARTIAL_WAKE_LOCK partial wake lock} when necessary.
+ * Note that since the purpose of doze mode is to conserve power (especially when
+ * running on battery), the dream should not wake the CPU very often or keep it
+ * awake for very long.
+ * </p><p>
+ * It is a good idea to call this method some time after the dream's entry animation
+ * has completed and the dream is ready to doze. It is important to completely
+ * finish all of the work needed before dozing since the application processor may
+ * be suspended at any moment once this method is called unless other wake locks
+ * are being held.
+ * </p><p>
+ * Call {@link #stopDozing} or {@link #finish} to stop dozing.
+ * </p>
+ *
+ * @see #stopDozing
+ * @hide For use by system UI components only.
+ */
+ @UnsupportedAppUsage
+ public void startDozing() {
+ if (mCanDoze && !mDozing) {
+ mDozing = true;
+ updateDoze();
+ }
+ }
+
+ private void updateDoze() {
+ if (mDreamToken == null) {
+ Slog.w(TAG, "Updating doze without a dream token.");
+ return;
+ }
+
+ if (mDozing) {
+ try {
+ mDreamManager.startDozing(mDreamToken, mDozeScreenState, mDozeScreenBrightness);
+ } catch (RemoteException ex) {
+ // system server died
+ }
+ }
+ }
+
+ /**
+ * Stops dozing, returns to active dreaming.
+ * <p>
+ * This method reverses the effect of {@link #startDozing}. From this moment onward,
+ * the application processor will be kept awake as long as the dream is running
+ * or until the dream starts dozing again.
+ * </p>
+ *
+ * @see #startDozing
+ * @hide For use by system UI components only.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public void stopDozing() {
+ if (mDozing) {
+ mDozing = false;
+ try {
+ mDreamManager.stopDozing(mDreamToken);
+ } catch (RemoteException ex) {
+ // system server died
+ }
+ }
+ }
+
+ /**
+ * Returns true if the dream will allow the system to enter a low-power state while
+ * it is running without actually turning off the screen. Defaults to false,
+ * keeping the application processor awake while the dream is running.
+ *
+ * @return True if the dream is dozing.
+ *
+ * @see #setDozing(boolean)
+ * @hide For use by system UI components only.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public boolean isDozing() {
+ return mDozing;
+ }
+
+ /**
+ * Gets the screen state to use while dozing.
+ *
+ * @return The screen state to use while dozing, such as {@link Display#STATE_ON},
+ * {@link Display#STATE_DOZE}, {@link Display#STATE_DOZE_SUSPEND},
+ * {@link Display#STATE_ON_SUSPEND}, {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN}
+ * for the default behavior.
+ *
+ * @see #setDozeScreenState
+ * @hide For use by system UI components only.
+ */
+ public int getDozeScreenState() {
+ return mDozeScreenState;
+ }
+
+ /**
+ * Sets the screen state to use while dozing.
+ * <p>
+ * The value of this property determines the power state of the primary display
+ * once {@link #startDozing} has been called. The default value is
+ * {@link Display#STATE_UNKNOWN} which lets the system decide.
+ * The dream may set a different state before starting to doze and may
+ * perform transitions between states while dozing to conserve power and
+ * achieve various effects.
+ * </p><p>
+ * Some devices will have dedicated hardware ("Sidekick") to animate
+ * the display content while the CPU sleeps. If the dream and the hardware support
+ * this, {@link Display#STATE_ON_SUSPEND} or {@link Display#STATE_DOZE_SUSPEND}
+ * will switch control to the Sidekick.
+ * </p><p>
+ * If not using Sidekick, it is recommended that the state be set to
+ * {@link Display#STATE_DOZE_SUSPEND} once the dream has completely
+ * finished drawing and before it releases its wakelock
+ * to allow the display hardware to be fully suspended. While suspended,
+ * the display will preserve its on-screen contents.
+ * </p><p>
+ * If the doze suspend state is used, the dream must make sure to set the mode back
+ * to {@link Display#STATE_DOZE} or {@link Display#STATE_ON} before drawing again
+ * since the display updates may be ignored and not seen by the user otherwise.
+ * </p><p>
+ * The set of available display power states and their behavior while dozing is
+ * hardware dependent and may vary across devices. The dream may therefore
+ * need to be modified or configured to correctly support the hardware.
+ * </p>
+ *
+ * @param state The screen state to use while dozing, such as {@link Display#STATE_ON},
+ * {@link Display#STATE_DOZE}, {@link Display#STATE_DOZE_SUSPEND},
+ * {@link Display#STATE_ON_SUSPEND}, {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN}
+ * for the default behavior.
+ *
+ * @hide For use by system UI components only.
+ */
+ @UnsupportedAppUsage
+ public void setDozeScreenState(int state) {
+ if (mDozeScreenState != state) {
+ mDozeScreenState = state;
+ updateDoze();
+ }
+ }
+
+ /**
+ * Gets the screen brightness to use while dozing.
+ *
+ * @return The screen brightness while dozing as a value between
+ * {@link PowerManager#BRIGHTNESS_OFF} (0) and {@link PowerManager#BRIGHTNESS_ON} (255),
+ * or {@link PowerManager#BRIGHTNESS_DEFAULT} (-1) to ask the system to apply
+ * its default policy based on the screen state.
+ *
+ * @see #setDozeScreenBrightness
+ * @hide For use by system UI components only.
+ */
+ @UnsupportedAppUsage
+ public int getDozeScreenBrightness() {
+ return mDozeScreenBrightness;
+ }
+
+ /**
+ * Sets the screen brightness to use while dozing.
+ * <p>
+ * The value of this property determines the power state of the primary display
+ * once {@link #startDozing} has been called. The default value is
+ * {@link PowerManager#BRIGHTNESS_DEFAULT} which lets the system decide.
+ * The dream may set a different brightness before starting to doze and may adjust
+ * the brightness while dozing to conserve power and achieve various effects.
+ * </p><p>
+ * Note that dream may specify any brightness in the full 0-255 range, including
+ * values that are less than the minimum value for manual screen brightness
+ * adjustments by the user. In particular, the value may be set to 0 which may
+ * turn off the backlight entirely while still leaving the screen on although
+ * this behavior is device dependent and not guaranteed.
+ * </p><p>
+ * The available range of display brightness values and their behavior while dozing is
+ * hardware dependent and may vary across devices. The dream may therefore
+ * need to be modified or configured to correctly support the hardware.
+ * </p>
+ *
+ * @param brightness The screen brightness while dozing as a value between
+ * {@link PowerManager#BRIGHTNESS_OFF} (0) and {@link PowerManager#BRIGHTNESS_ON} (255),
+ * or {@link PowerManager#BRIGHTNESS_DEFAULT} (-1) to ask the system to apply
+ * its default policy based on the screen state.
+ *
+ * @hide For use by system UI components only.
+ */
+ @UnsupportedAppUsage
+ public void setDozeScreenBrightness(int brightness) {
+ if (brightness != PowerManager.BRIGHTNESS_DEFAULT) {
+ brightness = clampAbsoluteBrightness(brightness);
+ }
+ if (mDozeScreenBrightness != brightness) {
+ mDozeScreenBrightness = brightness;
+ updateDoze();
+ }
+ }
+
+ /**
+ * Called when this Dream is constructed.
+ */
+ @Override
+ public void onCreate() {
+ if (mDebug) Slog.v(TAG, "onCreate()");
+ super.onCreate();
+ }
+
+ /**
+ * Called when the dream's window has been created and is visible and animation may now begin.
+ */
+ public void onDreamingStarted() {
+ if (mDebug) Slog.v(TAG, "onDreamingStarted()");
+ // hook for subclasses
+ }
+
+ /**
+ * Called when this Dream is stopped, either by external request or by calling finish(),
+ * before the window has been removed.
+ */
+ public void onDreamingStopped() {
+ if (mDebug) Slog.v(TAG, "onDreamingStopped()");
+ // hook for subclasses
+ }
+
+ /**
+ * Called when the dream is being asked to stop itself and wake.
+ * <p>
+ * The default implementation simply calls {@link #finish} which ends the dream
+ * immediately. Subclasses may override this function to perform a smooth exit
+ * transition then call {@link #finish} afterwards.
+ * </p><p>
+ * Note that the dream will only be given a short period of time (currently about
+ * five seconds) to wake up. If the dream does not finish itself in a timely manner
+ * then the system will forcibly finish it once the time allowance is up.
+ * </p>
+ */
+ public void onWakeUp() {
+ finish();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public final IBinder onBind(Intent intent) {
+ if (mDebug) Slog.v(TAG, "onBind() intent = " + intent);
+ mDreamServiceWrapper = new DreamServiceWrapper();
+ return mDreamServiceWrapper;
+ }
+
+ /**
+ * Stops the dream and detaches from the window.
+ * <p>
+ * When the dream ends, the system will be allowed to go to sleep fully unless there
+ * is a reason for it to be awake such as recent user activity or wake locks being held.
+ * </p>
+ */
+ public final void finish() {
+ if (mDebug) Slog.v(TAG, "finish(): mFinished=" + mFinished);
+
+ Activity activity = mActivity;
+ if (activity != null) {
+ if (!activity.isFinishing()) {
+ // In case the activity is not finished yet, do it now.
+ activity.finishAndRemoveTask();
+ }
+ return;
+ }
+
+ if (mFinished) {
+ return;
+ }
+ mFinished = true;
+
+ if (mDreamToken == null) {
+ Slog.w(TAG, "Finish was called before the dream was attached.");
+ stopSelf();
+ return;
+ }
+
+ try {
+ // finishSelf will unbind the dream controller from the dream service. This will
+ // trigger DreamService.this.onDestroy and DreamService.this will die.
+ mDreamManager.finishSelf(mDreamToken, true /*immediate*/);
+ } catch (RemoteException ex) {
+ // system server died
+ }
+ }
+
+ /**
+ * Wakes the dream up gently.
+ * <p>
+ * Calls {@link #onWakeUp} to give the dream a chance to perform an exit transition.
+ * When the transition is over, the dream should call {@link #finish}.
+ * </p>
+ */
+ public final void wakeUp() {
+ wakeUp(false);
+ }
+
+ private void wakeUp(boolean fromSystem) {
+ if (mDebug) Slog.v(TAG, "wakeUp(): fromSystem=" + fromSystem
+ + ", mWaking=" + mWaking + ", mFinished=" + mFinished);
+
+ if (!mWaking && !mFinished) {
+ mWaking = true;
+
+ // As a minor optimization, invoke the callback first in case it simply
+ // calls finish() immediately so there wouldn't be much point in telling
+ // the system that we are finishing the dream gently.
+ onWakeUp();
+
+ // Now tell the system we are waking gently, unless we already told
+ // it we were finishing immediately.
+ if (!fromSystem && !mFinished) {
+ if (mActivity == null) {
+ Slog.w(TAG, "WakeUp was called before the dream was attached.");
+ } else {
+ try {
+ mDreamManager.finishSelf(mDreamToken, false /*immediate*/);
+ } catch (RemoteException ex) {
+ // system server died
+ }
+ }
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onDestroy() {
+ if (mDebug) Slog.v(TAG, "onDestroy()");
+ // hook for subclasses
+
+ // Just in case destroy came in before detach, let's take care of that now
+ detach();
+
+ super.onDestroy();
+ }
+
+ // end public api
+
+ /**
+ * Called by DreamController.stopDream() when the Dream is about to be unbound and destroyed.
+ *
+ * Must run on mHandler.
+ */
+ private final void detach() {
+ if (mStarted) {
+ if (mDebug) Slog.v(TAG, "detach(): Calling onDreamingStopped()");
+ mStarted = false;
+ onDreamingStopped();
+ }
+
+ if (mActivity != null && !mActivity.isFinishing()) {
+ mActivity.finishAndRemoveTask();
+ } else {
+ finish();
+ }
+
+ mDreamToken = null;
+ mCanDoze = false;
+ }
+
+ /**
+ * Called when the Dream is ready to be shown.
+ *
+ * Must run on mHandler.
+ *
+ * @param dreamToken Token for this dream service.
+ * @param started A callback that will be invoked once onDreamingStarted has completed.
+ */
+ private void attach(IBinder dreamToken, boolean canDoze, IRemoteCallback started) {
+ if (mDreamToken != null) {
+ Slog.e(TAG, "attach() called when dream with token=" + mDreamToken
+ + " already attached");
+ return;
+ }
+ if (mFinished || mWaking) {
+ Slog.w(TAG, "attach() called after dream already finished");
+ try {
+ mDreamManager.finishSelf(dreamToken, true /*immediate*/);
+ } catch (RemoteException ex) {
+ // system server died
+ }
+ return;
+ }
+
+ mDreamToken = dreamToken;
+ mCanDoze = canDoze;
+ if (mWindowless && !mCanDoze) {
+ throw new IllegalStateException("Only doze dreams can be windowless");
+ }
+
+ mDispatchAfterOnAttachedToWindow = () -> {
+ if (mWindow != null || mWindowless) {
+ mStarted = true;
+ try {
+ onDreamingStarted();
+ } finally {
+ try {
+ started.sendResult(null);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+ };
+
+ // We need to defer calling onDreamingStarted until after the activity is created.
+ // If the dream is windowless, we can call it immediately. Otherwise, we wait
+ // for the DreamActivity to report onActivityCreated via
+ // DreamServiceWrapper.onActivityCreated.
+ if (!mWindowless) {
+ Intent i = new Intent(this, DreamActivity.class);
+ i.setPackage(getApplicationContext().getPackageName());
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ i.putExtra(DreamActivity.EXTRA_CALLBACK, mDreamServiceWrapper);
+
+ try {
+ if (!ActivityTaskManager.getService().startDreamActivity(i)) {
+ detach();
+ return;
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Could not connect to activity task manager to start dream activity");
+ e.rethrowFromSystemServer();
+ }
+ } else {
+ mDispatchAfterOnAttachedToWindow.run();
+ }
+ }
+
+ private void onWindowCreated(Window w) {
+ mWindow = w;
+ mWindow.setCallback(this);
+ mWindow.requestFeature(Window.FEATURE_NO_TITLE);
+
+ WindowManager.LayoutParams lp = mWindow.getAttributes();
+ lp.windowAnimations = com.android.internal.R.style.Animation_Dream;
+ lp.flags |= (WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
+ | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+ | (mFullscreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : 0)
+ | (mScreenBright ? WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON : 0)
+ );
+ mWindow.setAttributes(lp);
+ // Workaround: Currently low-profile and in-window system bar backgrounds don't go
+ // along well. Dreams usually don't need such bars anyways, so disable them by default.
+ mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+
+ mWindow.getDecorView().addOnAttachStateChangeListener(
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ mDispatchAfterOnAttachedToWindow.run();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ mActivity = null;
+ finish();
+ }
+ });
+ }
+
+ private boolean getWindowFlagValue(int flag, boolean defaultValue) {
+ return mWindow == null ? defaultValue : (mWindow.getAttributes().flags & flag) != 0;
+ }
+
+ private void applyWindowFlags(int flags, int mask) {
+ if (mWindow != null) {
+ WindowManager.LayoutParams lp = mWindow.getAttributes();
+ lp.flags = applyFlags(lp.flags, flags, mask);
+ mWindow.setAttributes(lp);
+ mWindow.getWindowManager().updateViewLayout(mWindow.getDecorView(), lp);
+ }
+ }
+
+ private int applyFlags(int oldFlags, int flags, int mask) {
+ return (oldFlags&~mask) | (flags&mask);
+ }
+
+ @Override
+ protected void dump(final FileDescriptor fd, PrintWriter pw, final String[] args) {
+ DumpUtils.dumpAsync(mHandler, new Dump() {
+ @Override
+ public void dump(PrintWriter pw, String prefix) {
+ dumpOnHandler(fd, pw, args);
+ }
+ }, pw, "", 1000);
+ }
+
+ /** @hide */
+ protected void dumpOnHandler(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.print(TAG + ": ");
+ if (mFinished) {
+ pw.println("stopped");
+ } else {
+ pw.println("running (dreamToken=" + mDreamToken + ")");
+ }
+ pw.println(" window: " + mWindow);
+ pw.print(" flags:");
+ if (isInteractive()) pw.print(" interactive");
+ if (isFullscreen()) pw.print(" fullscreen");
+ if (isScreenBright()) pw.print(" bright");
+ if (isWindowless()) pw.print(" windowless");
+ if (isDozing()) pw.print(" dozing");
+ else if (canDoze()) pw.print(" candoze");
+ pw.println();
+ if (canDoze()) {
+ pw.println(" doze screen state: " + Display.stateToString(mDozeScreenState));
+ pw.println(" doze screen brightness: " + mDozeScreenBrightness);
+ }
+ }
+
+ private static int clampAbsoluteBrightness(int value) {
+ return MathUtils.constrain(value, PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON);
+ }
+
+ /**
+ * The DreamServiceWrapper is used as a gateway to the system_server, where DreamController
+ * uses it to control the DreamService. It is also used to receive callbacks from the
+ * DreamActivity.
+ */
+ final class DreamServiceWrapper extends IDreamService.Stub {
+ @Override
+ public void attach(final IBinder dreamToken, final boolean canDoze,
+ IRemoteCallback started) {
+ mHandler.post(() -> DreamService.this.attach(dreamToken, canDoze, started));
+ }
+
+ @Override
+ public void detach() {
+ mHandler.post(DreamService.this::detach);
+ }
+
+ @Override
+ public void wakeUp() {
+ mHandler.post(() -> DreamService.this.wakeUp(true /*fromSystem*/));
+ }
+
+ /** @hide */
+ void onActivityCreated(DreamActivity a) {
+ mActivity = a;
+ onWindowCreated(a.getWindow());
+ }
+ }
+}
diff --git a/android/service/dreams/Sandman.java b/android/service/dreams/Sandman.java
new file mode 100644
index 0000000..f2cedbc
--- /dev/null
+++ b/android/service/dreams/Sandman.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2012 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.dreams;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Slog;
+
+/**
+ * Internal helper for launching dreams to ensure consistency between the
+ * <code>UiModeManagerService</code> system service and the <code>Somnambulator</code> activity.
+ *
+ * @hide
+ */
+public final class Sandman {
+ private static final String TAG = "Sandman";
+
+
+ // The sandman is eternal. No one instantiates him.
+ private Sandman() {
+ }
+
+ /**
+ * Returns true if the specified dock app intent should be started.
+ * False if we should dream instead, if appropriate.
+ */
+ public static boolean shouldStartDockApp(Context context, Intent intent) {
+ final ComponentName somnambulatorComponent = ComponentName.unflattenFromString(
+ context.getResources().getString(
+ com.android.internal.R.string.config_somnambulatorComponent));
+ ComponentName name = intent.resolveActivity(context.getPackageManager());
+ return name != null && !name.equals(somnambulatorComponent);
+ }
+
+ /**
+ * Starts a dream manually.
+ */
+ public static void startDreamByUserRequest(Context context) {
+ startDream(context, false);
+ }
+
+ /**
+ * Starts a dream when docked if the system has been configured to do so,
+ * otherwise does nothing.
+ */
+ public static void startDreamWhenDockedIfAppropriate(Context context) {
+ if (!isScreenSaverEnabled(context)
+ || !isScreenSaverActivatedOnDock(context)) {
+ Slog.i(TAG, "Dreams currently disabled for docks.");
+ return;
+ }
+
+ startDream(context, true);
+ }
+
+ private static void startDream(Context context, boolean docked) {
+ try {
+ IDreamManager dreamManagerService = IDreamManager.Stub.asInterface(
+ ServiceManager.getService(DreamService.DREAM_SERVICE));
+ if (dreamManagerService != null && !dreamManagerService.isDreaming()) {
+ if (docked) {
+ Slog.i(TAG, "Activating dream while docked.");
+
+ // Wake up.
+ // The power manager will wake up the system automatically when it starts
+ // receiving power from a dock but there is a race between that happening
+ // and the UI mode manager starting a dream. We want the system to already
+ // be awake by the time this happens. Otherwise the dream may not start.
+ PowerManager powerManager =
+ context.getSystemService(PowerManager.class);
+ powerManager.wakeUp(SystemClock.uptimeMillis(),
+ PowerManager.WAKE_REASON_PLUGGED_IN,
+ "android.service.dreams:DREAM");
+ } else {
+ Slog.i(TAG, "Activating dream by user request.");
+ }
+
+ // Dream.
+ dreamManagerService.dream();
+ }
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "Could not start dream when docked.", ex);
+ }
+ }
+
+ private static boolean isScreenSaverEnabled(Context context) {
+ int def = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_dreamsEnabledByDefault) ? 1 : 0;
+ return Settings.Secure.getIntForUser(context.getContentResolver(),
+ Settings.Secure.SCREENSAVER_ENABLED, def,
+ UserHandle.USER_CURRENT) != 0;
+ }
+
+ private static boolean isScreenSaverActivatedOnDock(Context context) {
+ int def = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault) ? 1 : 0;
+ return Settings.Secure.getIntForUser(context.getContentResolver(),
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, def,
+ UserHandle.USER_CURRENT) != 0;
+ }
+}
diff --git a/android/service/euicc/DownloadSubscriptionResult.java b/android/service/euicc/DownloadSubscriptionResult.java
new file mode 100644
index 0000000..3b1a2c9
--- /dev/null
+++ b/android/service/euicc/DownloadSubscriptionResult.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 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.euicc;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.euicc.EuiccService.ResolvableError;
+import android.service.euicc.EuiccService.Result;
+
+/**
+ * Result of a {@link EuiccService#onDownloadSubscription} operation.
+ * @hide
+ */
+@SystemApi
+public final class DownloadSubscriptionResult implements Parcelable {
+
+ public static final @android.annotation.NonNull Creator<DownloadSubscriptionResult> CREATOR =
+ new Creator<DownloadSubscriptionResult>() {
+ @Override
+ public DownloadSubscriptionResult createFromParcel(Parcel in) {
+ return new DownloadSubscriptionResult(in);
+ }
+
+ @Override
+ public DownloadSubscriptionResult[] newArray(int size) {
+ return new DownloadSubscriptionResult[size];
+ }
+ };
+
+ private final @Result int mResult;
+ private final @ResolvableError int mResolvableErrors;
+ private final int mCardId;
+
+ public DownloadSubscriptionResult(@Result int result, @ResolvableError int resolvableErrors,
+ int cardId) {
+ this.mResult = result;
+ this.mResolvableErrors = resolvableErrors;
+ this.mCardId = cardId;
+ }
+
+ /** Gets the result of the operation. */
+ public @Result int getResult() {
+ return mResult;
+ }
+
+ /**
+ * Gets the bit map of resolvable errors.
+ *
+ * <p>The value is passed from EuiccService. The values can be
+ *
+ * <ul>
+ * <li>{@link EuiccService#RESOLVABLE_ERROR_CONFIRMATION_CODE}
+ * <li>{@link EuiccService#RESOLVABLE_ERROR_POLICY_RULES}
+ * </ul>
+ */
+ public @ResolvableError int getResolvableErrors() {
+ return mResolvableErrors;
+ }
+
+ /**
+ * Gets the card Id. This is used when resolving resolvable errors. The value is passed from
+ * EuiccService.
+ */
+ public int getCardId() {
+ return mCardId;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mResult);
+ dest.writeInt(mResolvableErrors);
+ dest.writeInt(mCardId);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private DownloadSubscriptionResult(Parcel in) {
+ this.mResult = in.readInt();
+ this.mResolvableErrors = in.readInt();
+ this.mCardId = in.readInt();
+ }
+}
diff --git a/android/service/euicc/EuiccProfileInfo.java b/android/service/euicc/EuiccProfileInfo.java
new file mode 100644
index 0000000..8450a90
--- /dev/null
+++ b/android/service/euicc/EuiccProfileInfo.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2017 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.euicc;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.carrier.CarrierIdentifier;
+import android.telephony.UiccAccessRule;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Information about an embedded profile (subscription) on an eUICC.
+ *
+ * @hide
+ */
+@SystemApi
+public final class EuiccProfileInfo implements Parcelable {
+
+ /** Profile policy rules (bit mask) */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "POLICY_RULE_" }, value = {
+ POLICY_RULE_DO_NOT_DISABLE,
+ POLICY_RULE_DO_NOT_DELETE,
+ POLICY_RULE_DELETE_AFTER_DISABLING
+ })
+ /** @hide */
+ public @interface PolicyRule {}
+ /** Once this profile is enabled, it cannot be disabled. */
+ public static final int POLICY_RULE_DO_NOT_DISABLE = 1;
+ /** This profile cannot be deleted. */
+ public static final int POLICY_RULE_DO_NOT_DELETE = 1 << 1;
+ /** This profile should be deleted after being disabled. */
+ public static final int POLICY_RULE_DELETE_AFTER_DISABLING = 1 << 2;
+
+ /** Class of the profile */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "PROFILE_CLASS_" }, value = {
+ PROFILE_CLASS_TESTING,
+ PROFILE_CLASS_PROVISIONING,
+ PROFILE_CLASS_OPERATIONAL,
+ PROFILE_CLASS_UNSET
+ })
+ /** @hide */
+ public @interface ProfileClass {}
+ /** Testing profiles */
+ public static final int PROFILE_CLASS_TESTING = 0;
+ /** Provisioning profiles which are pre-loaded on eUICC */
+ public static final int PROFILE_CLASS_PROVISIONING = 1;
+ /** Operational profiles which can be pre-loaded or downloaded */
+ public static final int PROFILE_CLASS_OPERATIONAL = 2;
+ /**
+ * Profile class not set.
+ * @hide
+ */
+ public static final int PROFILE_CLASS_UNSET = -1;
+
+ /** State of the profile */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "PROFILE_STATE_" }, value = {
+ PROFILE_STATE_DISABLED,
+ PROFILE_STATE_ENABLED,
+ PROFILE_STATE_UNSET
+ })
+ /** @hide */
+ public @interface ProfileState {}
+ /** Disabled profiles */
+ public static final int PROFILE_STATE_DISABLED = 0;
+ /** Enabled profile */
+ public static final int PROFILE_STATE_ENABLED = 1;
+ /**
+ * Profile state not set.
+ * @hide
+ */
+ public static final int PROFILE_STATE_UNSET = -1;
+
+ /** The iccid of the subscription. */
+ private final String mIccid;
+
+ /** An optional nickname for the subscription. */
+ private final @Nullable String mNickname;
+
+ /** The service provider name for the subscription. */
+ private final String mServiceProviderName;
+
+ /** The profile name for the subscription. */
+ private final String mProfileName;
+
+ /** Profile class for the subscription. */
+ @ProfileClass private final int mProfileClass;
+
+ /** The profile state of the subscription. */
+ @ProfileState private final int mState;
+
+ /** The operator Id of the subscription. */
+ private final CarrierIdentifier mCarrierIdentifier;
+
+ /** The policy rules of the subscription. */
+ @PolicyRule private final int mPolicyRules;
+
+ /**
+ * Optional access rules defining which apps can manage this subscription. If unset, only the
+ * platform can manage it.
+ */
+ private final @Nullable UiccAccessRule[] mAccessRules;
+
+ public static final @android.annotation.NonNull Creator<EuiccProfileInfo> CREATOR = new Creator<EuiccProfileInfo>() {
+ @Override
+ public EuiccProfileInfo createFromParcel(Parcel in) {
+ return new EuiccProfileInfo(in);
+ }
+
+ @Override
+ public EuiccProfileInfo[] newArray(int size) {
+ return new EuiccProfileInfo[size];
+ }
+ };
+
+ // TODO(b/70292228): Remove this method when LPA can be updated.
+ /**
+ * @hide
+ * @deprecated - Do not use.
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public EuiccProfileInfo(String iccid, @Nullable UiccAccessRule[] accessRules,
+ @Nullable String nickname) {
+ if (!TextUtils.isDigitsOnly(iccid)) {
+ throw new IllegalArgumentException("iccid contains invalid characters: " + iccid);
+ }
+ this.mIccid = iccid;
+ this.mAccessRules = accessRules;
+ this.mNickname = nickname;
+
+ this.mServiceProviderName = null;
+ this.mProfileName = null;
+ this.mProfileClass = PROFILE_CLASS_UNSET;
+ this.mState = PROFILE_STATE_UNSET;
+ this.mCarrierIdentifier = null;
+ this.mPolicyRules = 0;
+ }
+
+ private EuiccProfileInfo(Parcel in) {
+ mIccid = in.readString();
+ mNickname = in.readString();
+ mServiceProviderName = in.readString();
+ mProfileName = in.readString();
+ mProfileClass = in.readInt();
+ mState = in.readInt();
+ byte exist = in.readByte();
+ if (exist == (byte) 1) {
+ mCarrierIdentifier = CarrierIdentifier.CREATOR.createFromParcel(in);
+ } else {
+ mCarrierIdentifier = null;
+ }
+ mPolicyRules = in.readInt();
+ mAccessRules = in.createTypedArray(UiccAccessRule.CREATOR);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mIccid);
+ dest.writeString(mNickname);
+ dest.writeString(mServiceProviderName);
+ dest.writeString(mProfileName);
+ dest.writeInt(mProfileClass);
+ dest.writeInt(mState);
+ if (mCarrierIdentifier != null) {
+ dest.writeByte((byte) 1);
+ mCarrierIdentifier.writeToParcel(dest, flags);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ dest.writeInt(mPolicyRules);
+ dest.writeTypedArray(mAccessRules, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** The builder to build a new {@link EuiccProfileInfo} instance. */
+ public static final class Builder {
+ private String mIccid;
+ private List<UiccAccessRule> mAccessRules;
+ private String mNickname;
+ private String mServiceProviderName;
+ private String mProfileName;
+ @ProfileClass private int mProfileClass;
+ @ProfileState private int mState;
+ private CarrierIdentifier mCarrierIdentifier;
+ @PolicyRule private int mPolicyRules;
+
+ public Builder(String value) {
+ if (!TextUtils.isDigitsOnly(value)) {
+ throw new IllegalArgumentException("iccid contains invalid characters: " + value);
+ }
+ mIccid = value;
+ }
+
+ public Builder(EuiccProfileInfo baseProfile) {
+ mIccid = baseProfile.mIccid;
+ mNickname = baseProfile.mNickname;
+ mServiceProviderName = baseProfile.mServiceProviderName;
+ mProfileName = baseProfile.mProfileName;
+ mProfileClass = baseProfile.mProfileClass;
+ mState = baseProfile.mState;
+ mCarrierIdentifier = baseProfile.mCarrierIdentifier;
+ mPolicyRules = baseProfile.mPolicyRules;
+ mAccessRules = Arrays.asList(baseProfile.mAccessRules);
+ }
+
+ /** Builds the profile instance. */
+ public EuiccProfileInfo build() {
+ if (mIccid == null) {
+ throw new IllegalStateException("ICCID must be set for a profile.");
+ }
+ return new EuiccProfileInfo(
+ mIccid,
+ mNickname,
+ mServiceProviderName,
+ mProfileName,
+ mProfileClass,
+ mState,
+ mCarrierIdentifier,
+ mPolicyRules,
+ mAccessRules);
+ }
+
+ /** Sets the iccId of the subscription. */
+ public Builder setIccid(String value) {
+ if (!TextUtils.isDigitsOnly(value)) {
+ throw new IllegalArgumentException("iccid contains invalid characters: " + value);
+ }
+ mIccid = value;
+ return this;
+ }
+
+ /** Sets the nickname of the subscription. */
+ public Builder setNickname(String value) {
+ mNickname = value;
+ return this;
+ }
+
+ /** Sets the service provider name of the subscription. */
+ public Builder setServiceProviderName(String value) {
+ mServiceProviderName = value;
+ return this;
+ }
+
+ /** Sets the profile name of the subscription. */
+ public Builder setProfileName(String value) {
+ mProfileName = value;
+ return this;
+ }
+
+ /** Sets the profile class of the subscription. */
+ public Builder setProfileClass(@ProfileClass int value) {
+ mProfileClass = value;
+ return this;
+ }
+
+ /** Sets the state of the subscription. */
+ public Builder setState(@ProfileState int value) {
+ mState = value;
+ return this;
+ }
+
+ /** Sets the carrier identifier of the subscription. */
+ public Builder setCarrierIdentifier(CarrierIdentifier value) {
+ mCarrierIdentifier = value;
+ return this;
+ }
+
+ /** Sets the policy rules of the subscription. */
+ public Builder setPolicyRules(@PolicyRule int value) {
+ mPolicyRules = value;
+ return this;
+ }
+
+ /** Sets the access rules of the subscription. */
+ public Builder setUiccAccessRule(@Nullable List<UiccAccessRule> value) {
+ mAccessRules = value;
+ return this;
+ }
+ }
+
+ private EuiccProfileInfo(
+ String iccid,
+ @Nullable String nickname,
+ String serviceProviderName,
+ String profileName,
+ @ProfileClass int profileClass,
+ @ProfileState int state,
+ CarrierIdentifier carrierIdentifier,
+ @PolicyRule int policyRules,
+ @Nullable List<UiccAccessRule> accessRules) {
+ this.mIccid = iccid;
+ this.mNickname = nickname;
+ this.mServiceProviderName = serviceProviderName;
+ this.mProfileName = profileName;
+ this.mProfileClass = profileClass;
+ this.mState = state;
+ this.mCarrierIdentifier = carrierIdentifier;
+ this.mPolicyRules = policyRules;
+ if (accessRules != null && accessRules.size() > 0) {
+ this.mAccessRules = accessRules.toArray(new UiccAccessRule[accessRules.size()]);
+ } else {
+ this.mAccessRules = null;
+ }
+ }
+
+ /** Gets the ICCID string. */
+ public String getIccid() {
+ return mIccid;
+ }
+
+ /** Gets the access rules. */
+ @Nullable
+ public List<UiccAccessRule> getUiccAccessRules() {
+ if (mAccessRules == null) return null;
+ return Arrays.asList(mAccessRules);
+ }
+
+ /** Gets the nickname. */
+ @Nullable
+ public String getNickname() {
+ return mNickname;
+ }
+
+ /** Gets the service provider name. */
+ public String getServiceProviderName() {
+ return mServiceProviderName;
+ }
+
+ /** Gets the profile name. */
+ public String getProfileName() {
+ return mProfileName;
+ }
+
+ /** Gets the profile class. */
+ @ProfileClass
+ public int getProfileClass() {
+ return mProfileClass;
+ }
+
+ /** Gets the state of the subscription. */
+ @ProfileState
+ public int getState() {
+ return mState;
+ }
+
+ /** Gets the carrier identifier. */
+ public CarrierIdentifier getCarrierIdentifier() {
+ return mCarrierIdentifier;
+ }
+
+ /** Gets the policy rules. */
+ @PolicyRule
+ public int getPolicyRules() {
+ return mPolicyRules;
+ }
+
+ /** Returns whether any policy rule exists. */
+ public boolean hasPolicyRules() {
+ return mPolicyRules != 0;
+ }
+
+ /** Checks whether a certain policy rule exists. */
+ public boolean hasPolicyRule(@PolicyRule int policy) {
+ return (mPolicyRules & policy) != 0;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ EuiccProfileInfo that = (EuiccProfileInfo) obj;
+ return Objects.equals(mIccid, that.mIccid)
+ && Objects.equals(mNickname, that.mNickname)
+ && Objects.equals(mServiceProviderName, that.mServiceProviderName)
+ && Objects.equals(mProfileName, that.mProfileName)
+ && mProfileClass == that.mProfileClass
+ && mState == that.mState
+ && Objects.equals(mCarrierIdentifier, that.mCarrierIdentifier)
+ && mPolicyRules == that.mPolicyRules
+ && Arrays.equals(mAccessRules, that.mAccessRules);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + Objects.hashCode(mIccid);
+ result = 31 * result + Objects.hashCode(mNickname);
+ result = 31 * result + Objects.hashCode(mServiceProviderName);
+ result = 31 * result + Objects.hashCode(mProfileName);
+ result = 31 * result + mProfileClass;
+ result = 31 * result + mState;
+ result = 31 * result + Objects.hashCode(mCarrierIdentifier);
+ result = 31 * result + mPolicyRules;
+ result = 31 * result + Arrays.hashCode(mAccessRules);
+ return result;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "EuiccProfileInfo (nickname="
+ + mNickname
+ + ", serviceProviderName="
+ + mServiceProviderName
+ + ", profileName="
+ + mProfileName
+ + ", profileClass="
+ + mProfileClass
+ + ", state="
+ + mState
+ + ", CarrierIdentifier="
+ + mCarrierIdentifier
+ + ", policyRules="
+ + mPolicyRules
+ + ", accessRules="
+ + Arrays.toString(mAccessRules)
+ + ")";
+ }
+}
diff --git a/android/service/euicc/EuiccService.java b/android/service/euicc/EuiccService.java
new file mode 100644
index 0000000..9315586
--- /dev/null
+++ b/android/service/euicc/EuiccService.java
@@ -0,0 +1,925 @@
+/*
+ * Copyright (C) 2017 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.euicc;
+
+import static android.telephony.euicc.EuiccCardManager.ResetOption;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.telephony.TelephonyManager;
+import android.telephony.euicc.DownloadableSubscription;
+import android.telephony.euicc.EuiccInfo;
+import android.telephony.euicc.EuiccManager;
+import android.telephony.euicc.EuiccManager.OtaStatus;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Service interface linking the system with an eUICC local profile assistant (LPA) application.
+ *
+ * <p>An LPA consists of two separate components (which may both be implemented in the same APK):
+ * the LPA backend, and the LPA UI or LUI.
+ *
+ * <p>To implement the LPA backend, you must extend this class and declare this service in your
+ * manifest file. The service must require the
+ * {@link android.Manifest.permission#BIND_EUICC_SERVICE} permission and include an intent filter
+ * with the {@link #EUICC_SERVICE_INTERFACE} action. It's suggested that the priority of the intent
+ * filter to be set to a non-zero value in case multiple implementations are present on the device.
+ * See the below example. Note that there will be problem if two LPAs are present and they have the
+ * same priority.
+ * Example:
+ *
+ * <pre>{@code
+ * <service android:name=".MyEuiccService"
+ * android:permission="android.permission.BIND_EUICC_SERVICE">
+ * <intent-filter android:priority="100">
+ * <action android:name="android.service.euicc.EuiccService" />
+ * </intent-filter>
+ * </service>
+ * }</pre>
+ *
+ * <p>To implement the LUI, you must provide an activity for the following actions:
+ *
+ * <ul>
+ * <li>{@link #ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS}
+ * <li>{@link #ACTION_PROVISION_EMBEDDED_SUBSCRIPTION}
+ * </ul>
+ *
+ * <p>As with the service, each activity must require the
+ * {@link android.Manifest.permission#BIND_EUICC_SERVICE} permission. Each should have an intent
+ * filter with the appropriate action, the {@link #CATEGORY_EUICC_UI} category, and a non-zero
+ * priority.
+ *
+ * <p>Old implementations of EuiccService may support passing in slot IDs equal to
+ * {@link android.telephony.SubscriptionManager#INVALID_SIM_SLOT_INDEX}, which allows the LPA to
+ * decide which eUICC to target when there are multiple eUICCs. This behavior is not supported in
+ * Android Q or later.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class EuiccService extends Service {
+ private static final String TAG = "EuiccService";
+
+ /** Action which must be included in this service's intent filter. */
+ public static final String EUICC_SERVICE_INTERFACE = "android.service.euicc.EuiccService";
+
+ /** Category which must be defined to all UI actions, for efficient lookup. */
+ public static final String CATEGORY_EUICC_UI = "android.service.euicc.category.EUICC_UI";
+
+ // LUI actions. These are passthroughs of the corresponding EuiccManager actions.
+
+ /**
+ * Action used to bind the carrier app and get the activation code from the carrier app. This
+ * activation code will be used to download the eSIM profile during eSIM activation flow.
+ */
+ public static final String ACTION_BIND_CARRIER_PROVISIONING_SERVICE =
+ "android.service.euicc.action.BIND_CARRIER_PROVISIONING_SERVICE";
+
+ /**
+ * Intent action sent by the LPA to launch a carrier app Activity for eSIM activation, e.g. a
+ * carrier login screen. Carrier apps wishing to support this activation method must implement
+ * an Activity that responds to this intent action. Upon completion, the Activity must return
+ * one of the following results to the LPA:
+ *
+ * <p>{@code Activity.RESULT_CANCELED}: The LPA should treat this as an back button and abort
+ * the activation flow.
+ * <p>{@code Activity.RESULT_OK}: The LPA should try to get an activation code from the carrier
+ * app by binding to the carrier app service implementing
+ * {@link #ACTION_BIND_CARRIER_PROVISIONING_SERVICE}.
+ * <p>{@code Activity.RESULT_OK} with
+ * {@link android.telephony.euicc.EuiccManager#EXTRA_USE_QR_SCANNER} set to true: The LPA should
+ * start a QR scanner for the user to scan an eSIM profile QR code.
+ * <p>For other results: The LPA should treat this as an error.
+ **/
+ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_START_CARRIER_ACTIVATION =
+ "android.service.euicc.action.START_CARRIER_ACTIVATION";
+
+ /**
+ * @see android.telephony.euicc.EuiccManager#ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS
+ * The difference is this one is used by system to bring up the LUI.
+ */
+ public static final String ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS =
+ "android.service.euicc.action.MANAGE_EMBEDDED_SUBSCRIPTIONS";
+
+ /** @see android.telephony.euicc.EuiccManager#ACTION_PROVISION_EMBEDDED_SUBSCRIPTION */
+ public static final String ACTION_PROVISION_EMBEDDED_SUBSCRIPTION =
+ "android.service.euicc.action.PROVISION_EMBEDDED_SUBSCRIPTION";
+
+ /**
+ * @see android.telephony.euicc.EuiccManager#ACTION_TOGGLE_SUBSCRIPTION_PRIVILEGED. This is
+ * a protected intent that can only be sent by the system, and requires the
+ * {@link android.Manifest.permission#BIND_EUICC_SERVICE} permission.
+ */
+ public static final String ACTION_TOGGLE_SUBSCRIPTION_PRIVILEGED =
+ "android.service.euicc.action.TOGGLE_SUBSCRIPTION_PRIVILEGED";
+
+ /**
+ * @see android.telephony.euicc.EuiccManager#ACTION_DELETE_SUBSCRIPTION_PRIVILEGED. This is
+ * a protected intent that can only be sent by the system, and requires the
+ * {@link android.Manifest.permission#BIND_EUICC_SERVICE} permission.
+ */
+ public static final String ACTION_DELETE_SUBSCRIPTION_PRIVILEGED =
+ "android.service.euicc.action.DELETE_SUBSCRIPTION_PRIVILEGED";
+
+ /**
+ * @see android.telephony.euicc.EuiccManager#ACTION_RENAME_SUBSCRIPTION_PRIVILEGED. This is
+ * a protected intent that can only be sent by the system, and requires the
+ * {@link android.Manifest.permission#BIND_EUICC_SERVICE} permission.
+ */
+ public static final String ACTION_RENAME_SUBSCRIPTION_PRIVILEGED =
+ "android.service.euicc.action.RENAME_SUBSCRIPTION_PRIVILEGED";
+
+ /**
+ * @see android.telephony.euicc.EuiccManager#ACTION_START_EUICC_ACTIVATION. This is
+ * a protected intent that can only be sent by the system, and requires the
+ * {@link android.Manifest.permission#BIND_EUICC_SERVICE} permission.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_START_EUICC_ACTIVATION =
+ "android.service.euicc.action.START_EUICC_ACTIVATION";
+
+ // LUI resolution actions. These are called by the platform to resolve errors in situations that
+ // require user interaction.
+ // TODO(b/33075886): Define extras for any input parameters to these dialogs once they are
+ // more scoped out.
+ /**
+ * Alert the user that this action will result in an active SIM being deactivated.
+ * To implement the LUI triggered by the system, you need to define this in AndroidManifest.xml.
+ */
+ public static final String ACTION_RESOLVE_DEACTIVATE_SIM =
+ "android.service.euicc.action.RESOLVE_DEACTIVATE_SIM";
+ /**
+ * Alert the user about a download/switch being done for an app that doesn't currently have
+ * carrier privileges.
+ */
+ public static final String ACTION_RESOLVE_NO_PRIVILEGES =
+ "android.service.euicc.action.RESOLVE_NO_PRIVILEGES";
+
+ /**
+ * Ask the user to input carrier confirmation code.
+ *
+ * @deprecated From Q, the resolvable errors happened in the download step are presented as
+ * bit map in {@link #EXTRA_RESOLVABLE_ERRORS}. The corresponding action would be
+ * {@link #ACTION_RESOLVE_RESOLVABLE_ERRORS}.
+ */
+ @Deprecated
+ public static final String ACTION_RESOLVE_CONFIRMATION_CODE =
+ "android.service.euicc.action.RESOLVE_CONFIRMATION_CODE";
+
+ /** Ask the user to resolve all the resolvable errors. */
+ public static final String ACTION_RESOLVE_RESOLVABLE_ERRORS =
+ "android.service.euicc.action.RESOLVE_RESOLVABLE_ERRORS";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "RESOLVABLE_ERROR_" }, value = {
+ RESOLVABLE_ERROR_CONFIRMATION_CODE,
+ RESOLVABLE_ERROR_POLICY_RULES,
+ })
+ public @interface ResolvableError {}
+
+ /**
+ * Possible value for the bit map of resolvable errors indicating the download process needs
+ * the user to input confirmation code.
+ */
+ public static final int RESOLVABLE_ERROR_CONFIRMATION_CODE = 1 << 0;
+ /**
+ * Possible value for the bit map of resolvable errors indicating the download process needs
+ * the user's consent to allow profile policy rules.
+ */
+ public static final int RESOLVABLE_ERROR_POLICY_RULES = 1 << 1;
+
+ /**
+ * Intent extra set for resolution requests containing the package name of the calling app.
+ * This is used by the above actions including ACTION_RESOLVE_DEACTIVATE_SIM,
+ * ACTION_RESOLVE_NO_PRIVILEGES and ACTION_RESOLVE_RESOLVABLE_ERRORS.
+ */
+ public static final String EXTRA_RESOLUTION_CALLING_PACKAGE =
+ "android.service.euicc.extra.RESOLUTION_CALLING_PACKAGE";
+
+ /**
+ * Intent extra set for resolution requests containing the list of resolvable errors to be
+ * resolved. Each resolvable error is an integer. Its possible values include:
+ * <UL>
+ * <LI>{@link #RESOLVABLE_ERROR_CONFIRMATION_CODE}
+ * <LI>{@link #RESOLVABLE_ERROR_POLICY_RULES}
+ * </UL>
+ */
+ public static final String EXTRA_RESOLVABLE_ERRORS =
+ "android.service.euicc.extra.RESOLVABLE_ERRORS";
+
+ /**
+ * Intent extra set for resolution requests containing a boolean indicating whether to ask the
+ * user to retry another confirmation code.
+ */
+ public static final String EXTRA_RESOLUTION_CONFIRMATION_CODE_RETRIED =
+ "android.service.euicc.extra.RESOLUTION_CONFIRMATION_CODE_RETRIED";
+
+ /**
+ * Intent extra set for resolution requests containing an int indicating the current card Id.
+ */
+ public static final String EXTRA_RESOLUTION_CARD_ID =
+ "android.service.euicc.extra.RESOLUTION_CARD_ID";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "RESULT_" }, value = {
+ RESULT_OK,
+ RESULT_MUST_DEACTIVATE_SIM,
+ RESULT_RESOLVABLE_ERRORS,
+ RESULT_NEED_CONFIRMATION_CODE,
+ RESULT_FIRST_USER,
+ })
+ public @interface Result {}
+
+ /** Result code for a successful operation. */
+ public static final int RESULT_OK = 0;
+ /** Result code indicating that an active SIM must be deactivated to perform the operation. */
+ public static final int RESULT_MUST_DEACTIVATE_SIM = -1;
+ /** Result code indicating that the user must resolve resolvable errors. */
+ public static final int RESULT_RESOLVABLE_ERRORS = -2;
+ /**
+ * Result code indicating that the user must input a carrier confirmation code.
+ *
+ * @deprecated From Q, the resolvable errors happened in the download step are presented as
+ * bit map in {@link #EXTRA_RESOLVABLE_ERRORS}. The corresponding result would be
+ * {@link #RESULT_RESOLVABLE_ERRORS}.
+ */
+ @Deprecated
+ public static final int RESULT_NEED_CONFIRMATION_CODE = -2;
+ // New predefined codes should have negative values.
+
+ /** Start of implementation-specific error results. */
+ public static final int RESULT_FIRST_USER = 1;
+
+ /**
+ * Boolean extra for resolution actions indicating whether the user granted consent.
+ * This is used and set by the implementation and used in {@code EuiccOperation}.
+ */
+ public static final String EXTRA_RESOLUTION_CONSENT =
+ "android.service.euicc.extra.RESOLUTION_CONSENT";
+ /**
+ * String extra for resolution actions indicating the carrier confirmation code.
+ * This is used and set by the implementation and used in {@code EuiccOperation}.
+ */
+ public static final String EXTRA_RESOLUTION_CONFIRMATION_CODE =
+ "android.service.euicc.extra.RESOLUTION_CONFIRMATION_CODE";
+ /**
+ * String extra for resolution actions indicating whether the user allows policy rules.
+ * This is used and set by the implementation and used in {@code EuiccOperation}.
+ */
+ public static final String EXTRA_RESOLUTION_ALLOW_POLICY_RULES =
+ "android.service.euicc.extra.RESOLUTION_ALLOW_POLICY_RULES";
+
+ private final IEuiccService.Stub mStubWrapper;
+
+ private ThreadPoolExecutor mExecutor;
+
+ public EuiccService() {
+ mStubWrapper = new IEuiccServiceWrapper();
+ }
+
+ /**
+ * Given a SubjectCode[5.2.6.1] and ReasonCode[5.2.6.2] from GSMA (SGP.22 v2.2), encode it to
+ * the format described in
+ * {@link android.telephony.euicc.EuiccManager#OPERATION_SMDX_SUBJECT_REASON_CODE}
+ *
+ * @param subjectCode SubjectCode[5.2.6.1] from GSMA (SGP.22 v2.2)
+ * @param reasonCode ReasonCode[5.2.6.2] from GSMA (SGP.22 v2.2)
+ * @return encoded error code described in
+ * {@link android.telephony.euicc.EuiccManager#OPERATION_SMDX_SUBJECT_REASON_CODE}
+ * @throws NumberFormatException when the Subject/Reason code contains non digits
+ * @throws IllegalArgumentException when Subject/Reason code is null/empty
+ * @throws UnsupportedOperationException when sections has more than four layers (e.g 5.8.1.2)
+ * or when an number is bigger than 15
+ */
+ public int encodeSmdxSubjectAndReasonCode(@Nullable String subjectCode,
+ @Nullable String reasonCode)
+ throws NumberFormatException, IllegalArgumentException, UnsupportedOperationException {
+ final int maxSupportedSection = 3;
+ final int maxSupportedDigit = 15;
+ final int bitsPerSection = 4;
+
+ if (TextUtils.isEmpty(subjectCode) || TextUtils.isEmpty(reasonCode)) {
+ throw new IllegalArgumentException("SubjectCode/ReasonCode is empty");
+ }
+
+ final String[] subjectCodeToken = subjectCode.split("\\.");
+ final String[] reasonCodeToken = reasonCode.split("\\.");
+
+ if (subjectCodeToken.length > maxSupportedSection
+ || reasonCodeToken.length > maxSupportedSection) {
+ throw new UnsupportedOperationException("Only three nested layer is supported.");
+ }
+
+ int result = EuiccManager.OPERATION_SMDX_SUBJECT_REASON_CODE;
+
+ // Pad the 0s needed for subject code
+ result = result << (maxSupportedSection - subjectCodeToken.length) * bitsPerSection;
+
+ for (String digitString : subjectCodeToken) {
+ int num = Integer.parseInt(digitString);
+ if (num > maxSupportedDigit) {
+ throw new UnsupportedOperationException("SubjectCode exceeds " + maxSupportedDigit);
+ }
+ result = (result << bitsPerSection) + num;
+ }
+
+ // Pad the 0s needed for reason code
+ result = result << (maxSupportedSection - reasonCodeToken.length) * bitsPerSection;
+ for (String digitString : reasonCodeToken) {
+ int num = Integer.parseInt(digitString);
+ if (num > maxSupportedDigit) {
+ throw new UnsupportedOperationException("ReasonCode exceeds " + maxSupportedDigit);
+ }
+ result = (result << bitsPerSection) + num;
+ }
+
+ return result;
+ }
+
+ @Override
+ @CallSuper
+ public void onCreate() {
+ super.onCreate();
+ // We use a oneway AIDL interface to avoid blocking phone process binder threads on IPCs to
+ // an external process, but doing so means the requests are serialized by binder, which is
+ // not desired. Spin up a background thread pool to allow requests to be parallelized.
+ // TODO(b/38206971): Consider removing this if basic card-level functions like listing
+ // profiles are moved to the platform.
+ mExecutor = new ThreadPoolExecutor(
+ 4 /* corePoolSize */,
+ 4 /* maxPoolSize */,
+ 30, TimeUnit.SECONDS, /* keepAliveTime */
+ new LinkedBlockingQueue<>(), /* workQueue */
+ new ThreadFactory() {
+ private final AtomicInteger mCount = new AtomicInteger(1);
+
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, "EuiccService #" + mCount.getAndIncrement());
+ }
+ }
+ );
+ mExecutor.allowCoreThreadTimeOut(true);
+ }
+
+ @Override
+ @CallSuper
+ public void onDestroy() {
+ mExecutor.shutdownNow();
+ super.onDestroy();
+ }
+
+ /**
+ * If overriding this method, call through to the super method for any unknown actions.
+ * {@inheritDoc}
+ */
+ @Override
+ @CallSuper
+ public IBinder onBind(Intent intent) {
+ return mStubWrapper;
+ }
+
+ /**
+ * Callback class for {@link #onStartOtaIfNecessary(int, OtaStatusChangedCallback)}
+ *
+ * The status of OTA which can be {@code android.telephony.euicc.EuiccManager#EUICC_OTA_}
+ *
+ * @see IEuiccService#startOtaIfNecessary
+ */
+ public abstract static class OtaStatusChangedCallback {
+ /** Called when OTA status is changed. */
+ public abstract void onOtaStatusChanged(int status);
+ }
+
+ /**
+ * Return the EID of the eUICC.
+ *
+ * @param slotId ID of the SIM slot being queried.
+ * @return the EID.
+ * @see android.telephony.euicc.EuiccManager#getEid
+ */
+ // TODO(b/36260308): Update doc when we have multi-SIM support.
+ public abstract String onGetEid(int slotId);
+
+ /**
+ * Return the status of OTA update.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @return The status of Euicc OTA update.
+ * @see android.telephony.euicc.EuiccManager#getOtaStatus
+ */
+ public abstract @OtaStatus int onGetOtaStatus(int slotId);
+
+ /**
+ * Perform OTA if current OS is not the latest one.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @param statusChangedCallback Function called when OTA status changed.
+ */
+ public abstract void onStartOtaIfNecessary(
+ int slotId, OtaStatusChangedCallback statusChangedCallback);
+
+ /**
+ * Populate {@link DownloadableSubscription} metadata for the given downloadable subscription.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @param subscription A subscription whose metadata needs to be populated.
+ * @param forceDeactivateSim If true, and if an active SIM must be deactivated to access the
+ * eUICC, perform this action automatically. Otherwise, {@link #RESULT_MUST_DEACTIVATE_SIM)}
+ * should be returned to allow the user to consent to this operation first.
+ * @return The result of the operation.
+ * @see android.telephony.euicc.EuiccManager#getDownloadableSubscriptionMetadata
+ */
+ public abstract GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(
+ int slotId, DownloadableSubscription subscription, boolean forceDeactivateSim);
+
+ /**
+ * Return metadata for subscriptions which are available for download for this device.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @param forceDeactivateSim If true, and if an active SIM must be deactivated to access the
+ * eUICC, perform this action automatically. Otherwise, {@link #RESULT_MUST_DEACTIVATE_SIM)}
+ * should be returned to allow the user to consent to this operation first.
+ * @return The result of the list operation.
+ * @see android.telephony.euicc.EuiccManager#getDefaultDownloadableSubscriptionList
+ */
+ public abstract GetDefaultDownloadableSubscriptionListResult
+ onGetDefaultDownloadableSubscriptionList(int slotId, boolean forceDeactivateSim);
+
+ /**
+ * Download the given subscription.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @param subscription The subscription to download.
+ * @param switchAfterDownload If true, the subscription should be enabled upon successful
+ * download.
+ * @param forceDeactivateSim If true, and if an active SIM must be deactivated to access the
+ * eUICC, perform this action automatically. Otherwise, {@link #RESULT_MUST_DEACTIVATE_SIM}
+ * should be returned to allow the user to consent to this operation first.
+ * @param resolvedBundle The bundle containing information on resolved errors. It can contain
+ * a string of confirmation code for the key {@link #EXTRA_RESOLUTION_CONFIRMATION_CODE},
+ * and a boolean for key {@link #EXTRA_RESOLUTION_ALLOW_POLICY_RULES} indicating whether
+ * the user allows profile policy rules or not.
+ * @return a DownloadSubscriptionResult instance including a result code, a resolvable errors
+ * bit map, and original the card Id. The result code may be one of the predefined
+ * {@code RESULT_} constants or any implementation-specific code starting with
+ * {@link #RESULT_FIRST_USER}. The resolvable error bit map can be either 0 or values
+ * defined in {@code RESOLVABLE_ERROR_}. A subclass should override this method. Otherwise,
+ * this method does nothing and returns null by default.
+ * @see android.telephony.euicc.EuiccManager#downloadSubscription
+ */
+ public DownloadSubscriptionResult onDownloadSubscription(int slotId,
+ @NonNull DownloadableSubscription subscription, boolean switchAfterDownload,
+ boolean forceDeactivateSim, @Nullable Bundle resolvedBundle) {
+ return null;
+ }
+
+ /**
+ * Download the given subscription.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @param subscription The subscription to download.
+ * @param switchAfterDownload If true, the subscription should be enabled upon successful
+ * download.
+ * @param forceDeactivateSim If true, and if an active SIM must be deactivated to access the
+ * eUICC, perform this action automatically. Otherwise, {@link #RESULT_MUST_DEACTIVATE_SIM}
+ * should be returned to allow the user to consent to this operation first.
+ * @return the result of the download operation. May be one of the predefined {@code RESULT_}
+ * constants or any implementation-specific code starting with {@link #RESULT_FIRST_USER}.
+ * @see android.telephony.euicc.EuiccManager#downloadSubscription
+ *
+ * @deprecated From Q, a subclass should use and override the above
+ * {@link #onDownloadSubscription(int, DownloadableSubscription, boolean, boolean, Bundle)}. The
+ * default return value for this one is Integer.MIN_VALUE.
+ */
+ @Deprecated public @Result int onDownloadSubscription(int slotId,
+ @NonNull DownloadableSubscription subscription, boolean switchAfterDownload,
+ boolean forceDeactivateSim) {
+ return Integer.MIN_VALUE;
+ }
+
+ /**
+ * Return a list of all @link EuiccProfileInfo}s.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @return The result of the operation.
+ * @see android.telephony.SubscriptionManager#getAvailableSubscriptionInfoList
+ * @see android.telephony.SubscriptionManager#getAccessibleSubscriptionInfoList
+ */
+ public abstract @NonNull GetEuiccProfileInfoListResult onGetEuiccProfileInfoList(int slotId);
+
+ /**
+ * Return info about the eUICC chip/device.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @return the {@link EuiccInfo} for the eUICC chip/device.
+ * @see android.telephony.euicc.EuiccManager#getEuiccInfo
+ */
+ public abstract @NonNull EuiccInfo onGetEuiccInfo(int slotId);
+
+ /**
+ * Delete the given subscription.
+ *
+ * <p>If the subscription is currently active, it should be deactivated first (equivalent to a
+ * physical SIM being ejected).
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @param iccid the ICCID of the subscription to delete.
+ * @return the result of the delete operation. May be one of the predefined {@code RESULT_}
+ * constants or any implementation-specific code starting with {@link #RESULT_FIRST_USER}.
+ * @see android.telephony.euicc.EuiccManager#deleteSubscription
+ */
+ public abstract @Result int onDeleteSubscription(int slotId, String iccid);
+
+ /**
+ * Switch to the given subscription.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @param iccid the ICCID of the subscription to enable. May be null, in which case the current
+ * profile should be deactivated and no profile should be activated to replace it - this is
+ * equivalent to a physical SIM being ejected.
+ * @param forceDeactivateSim If true, and if an active SIM must be deactivated to access the
+ * eUICC, perform this action automatically. Otherwise, {@link #RESULT_MUST_DEACTIVATE_SIM}
+ * should be returned to allow the user to consent to this operation first.
+ * @return the result of the switch operation. May be one of the predefined {@code RESULT_}
+ * constants or any implementation-specific code starting with {@link #RESULT_FIRST_USER}.
+ * @see android.telephony.euicc.EuiccManager#switchToSubscription
+ */
+ public abstract @Result int onSwitchToSubscription(int slotId, @Nullable String iccid,
+ boolean forceDeactivateSim);
+
+ /**
+ * Update the nickname of the given subscription.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @param iccid the ICCID of the subscription to update.
+ * @param nickname the new nickname to apply.
+ * @return the result of the update operation. May be one of the predefined {@code RESULT_}
+ * constants or any implementation-specific code starting with {@link #RESULT_FIRST_USER}.
+ * @see android.telephony.euicc.EuiccManager#updateSubscriptionNickname
+ */
+ public abstract int onUpdateSubscriptionNickname(int slotId, String iccid,
+ String nickname);
+
+ /**
+ * Erase all operational subscriptions on the device.
+ *
+ * <p>This is intended to be used for device resets. As such, the reset should be performed even
+ * if an active SIM must be deactivated in order to access the eUICC.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @return the result of the erase operation. May be one of the predefined {@code RESULT_}
+ * constants or any implementation-specific code starting with {@link #RESULT_FIRST_USER}.
+ * @see android.telephony.euicc.EuiccManager#eraseSubscriptions
+ *
+ * @deprecated From R, callers should specify a flag for specific set of subscriptions to erase
+ * and use {@link #onEraseSubscriptions(int, int)} instead
+ */
+ @Deprecated
+ public abstract int onEraseSubscriptions(int slotId);
+
+ /**
+ * Erase specific subscriptions on the device.
+ *
+ * <p>This is intended to be used for device resets. As such, the reset should be performed even
+ * if an active SIM must be deactivated in order to access the eUICC.
+ *
+ * @param slotIndex index of the SIM slot to use for the operation.
+ * @param options flag for specific group of subscriptions to erase
+ * @return the result of the erase operation. May be one of the predefined {@code RESULT_}
+ * constants or any implementation-specific code starting with {@link #RESULT_FIRST_USER}.
+ * @see android.telephony.euicc.EuiccManager#eraseSubscriptionsWithOptions
+ */
+ public int onEraseSubscriptions(int slotIndex, @ResetOption int options) {
+ throw new UnsupportedOperationException(
+ "This method must be overridden to enable the ResetOption parameter");
+ }
+
+ /**
+ * Ensure that subscriptions will be retained on the next factory reset.
+ *
+ * <p>Called directly before a factory reset. Assumes that a normal factory reset will lead to
+ * profiles being erased on first boot (to cover fastboot/recovery wipes), so the implementation
+ * should persist some bit that will remain accessible after the factory reset to bypass this
+ * flow when this method is called.
+ *
+ * @param slotId ID of the SIM slot to use for the operation.
+ * @return the result of the operation. May be one of the predefined {@code RESULT_} constants
+ * or any implementation-specific code starting with {@link #RESULT_FIRST_USER}.
+ */
+ public abstract int onRetainSubscriptionsForFactoryReset(int slotId);
+
+ /**
+ * Dump to a provided printWriter.
+ */
+ public void dump(@NonNull PrintWriter printWriter) {
+ printWriter.println("The connected LPA does not implement EuiccService#dump()");
+ }
+
+ /**
+ * Wrapper around IEuiccService that forwards calls to implementations of {@link EuiccService}.
+ */
+ private class IEuiccServiceWrapper extends IEuiccService.Stub {
+ @Override
+ public void downloadSubscription(int slotId, DownloadableSubscription subscription,
+ boolean switchAfterDownload, boolean forceDeactivateSim, Bundle resolvedBundle,
+ IDownloadSubscriptionCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ DownloadSubscriptionResult result;
+ try {
+ result =
+ EuiccService.this.onDownloadSubscription(
+ slotId, subscription, switchAfterDownload, forceDeactivateSim,
+ resolvedBundle);
+ } catch (AbstractMethodError e) {
+ Log.w(TAG, "The new onDownloadSubscription(int, "
+ + "DownloadableSubscription, boolean, boolean, Bundle) is not "
+ + "implemented. Fall back to the old one.", e);
+ int resultCode = EuiccService.this.onDownloadSubscription(
+ slotId, subscription, switchAfterDownload, forceDeactivateSim);
+ result = new DownloadSubscriptionResult(resultCode,
+ 0 /* resolvableErrors */, TelephonyManager.UNSUPPORTED_CARD_ID);
+ }
+ try {
+ callback.onComplete(result);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void getEid(int slotId, IGetEidCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ String eid = EuiccService.this.onGetEid(slotId);
+ try {
+ callback.onSuccess(eid);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void startOtaIfNecessary(
+ int slotId, IOtaStatusChangedCallback statusChangedCallback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ EuiccService.this.onStartOtaIfNecessary(slotId, new OtaStatusChangedCallback() {
+ @Override
+ public void onOtaStatusChanged(int status) {
+ try {
+ statusChangedCallback.onOtaStatusChanged(status);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void getOtaStatus(int slotId, IGetOtaStatusCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ int status = EuiccService.this.onGetOtaStatus(slotId);
+ try {
+ callback.onSuccess(status);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void getDownloadableSubscriptionMetadata(int slotId,
+ DownloadableSubscription subscription,
+ boolean forceDeactivateSim,
+ IGetDownloadableSubscriptionMetadataCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ GetDownloadableSubscriptionMetadataResult result =
+ EuiccService.this.onGetDownloadableSubscriptionMetadata(
+ slotId, subscription, forceDeactivateSim);
+ try {
+ callback.onComplete(result);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void getDefaultDownloadableSubscriptionList(int slotId, boolean forceDeactivateSim,
+ IGetDefaultDownloadableSubscriptionListCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ GetDefaultDownloadableSubscriptionListResult result =
+ EuiccService.this.onGetDefaultDownloadableSubscriptionList(
+ slotId, forceDeactivateSim);
+ try {
+ callback.onComplete(result);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void getEuiccProfileInfoList(int slotId, IGetEuiccProfileInfoListCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ GetEuiccProfileInfoListResult result =
+ EuiccService.this.onGetEuiccProfileInfoList(slotId);
+ try {
+ callback.onComplete(result);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void getEuiccInfo(int slotId, IGetEuiccInfoCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ EuiccInfo euiccInfo = EuiccService.this.onGetEuiccInfo(slotId);
+ try {
+ callback.onSuccess(euiccInfo);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+
+ }
+
+ @Override
+ public void deleteSubscription(int slotId, String iccid,
+ IDeleteSubscriptionCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ int result = EuiccService.this.onDeleteSubscription(slotId, iccid);
+ try {
+ callback.onComplete(result);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void switchToSubscription(int slotId, String iccid, boolean forceDeactivateSim,
+ ISwitchToSubscriptionCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ int result =
+ EuiccService.this.onSwitchToSubscription(
+ slotId, iccid, forceDeactivateSim);
+ try {
+ callback.onComplete(result);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void updateSubscriptionNickname(int slotId, String iccid, String nickname,
+ IUpdateSubscriptionNicknameCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ int result =
+ EuiccService.this.onUpdateSubscriptionNickname(slotId, iccid, nickname);
+ try {
+ callback.onComplete(result);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void eraseSubscriptions(int slotId, IEraseSubscriptionsCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ int result = EuiccService.this.onEraseSubscriptions(slotId);
+ try {
+ callback.onComplete(result);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void eraseSubscriptionsWithOptions(
+ int slotIndex, @ResetOption int options, IEraseSubscriptionsCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ int result = EuiccService.this.onEraseSubscriptions(slotIndex, options);
+ try {
+ callback.onComplete(result);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void retainSubscriptionsForFactoryReset(int slotId,
+ IRetainSubscriptionsForFactoryResetCallback callback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ int result = EuiccService.this.onRetainSubscriptionsForFactoryReset(slotId);
+ try {
+ callback.onComplete(result);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+
+ @Override
+ public void dump(IEuiccServiceDumpResultCallback callback) throws RemoteException {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ final StringWriter sw = new StringWriter();
+ final PrintWriter pw = new PrintWriter(sw);
+ EuiccService.this.dump(pw);
+ callback.onComplete(sw.toString());
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/android/service/euicc/GetDefaultDownloadableSubscriptionListResult.java b/android/service/euicc/GetDefaultDownloadableSubscriptionListResult.java
new file mode 100644
index 0000000..2382f65
--- /dev/null
+++ b/android/service/euicc/GetDefaultDownloadableSubscriptionListResult.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 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.euicc;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.euicc.DownloadableSubscription;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Result of a {@link EuiccService#onGetDefaultDownloadableSubscriptionList} operation.
+ * @hide
+ */
+@SystemApi
+public final class GetDefaultDownloadableSubscriptionListResult implements Parcelable {
+
+ public static final @android.annotation.NonNull Creator<GetDefaultDownloadableSubscriptionListResult> CREATOR =
+ new Creator<GetDefaultDownloadableSubscriptionListResult>() {
+ @Override
+ public GetDefaultDownloadableSubscriptionListResult createFromParcel(Parcel in) {
+ return new GetDefaultDownloadableSubscriptionListResult(in);
+ }
+
+ @Override
+ public GetDefaultDownloadableSubscriptionListResult[] newArray(int size) {
+ return new GetDefaultDownloadableSubscriptionListResult[size];
+ }
+ };
+
+ /**
+ * @hide
+ * @deprecated - Do no use. Use getResult() instead.
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public final int result;
+
+ @Nullable
+ private final DownloadableSubscription[] mSubscriptions;
+
+ /**
+ * Gets the result of the operation.
+ *
+ * <p>May be one of the predefined {@code RESULT_} constants in EuiccService or any
+ * implementation-specific code starting with {@link EuiccService#RESULT_FIRST_USER}.
+ */
+ public int getResult() {
+ return result;
+ }
+
+ /**
+ * Gets the available {@link DownloadableSubscription}s (with filled-in metadata).
+ *
+ * <p>Only non-null if {@link #result} is {@link EuiccService#RESULT_OK}.
+ */
+ @Nullable
+ public List<DownloadableSubscription> getDownloadableSubscriptions() {
+ if (mSubscriptions == null) return null;
+ return Arrays.asList(mSubscriptions);
+ }
+
+ /**
+ * Construct a new {@link GetDefaultDownloadableSubscriptionListResult}.
+ *
+ * @param result Result of the operation. May be one of the predefined {@code RESULT_} constants
+ * in EuiccService or any implementation-specific code starting with
+ * {@link EuiccService#RESULT_FIRST_USER}.
+ * @param subscriptions The available subscriptions. Should only be provided if the result is
+ * {@link EuiccService#RESULT_OK}.
+ */
+ public GetDefaultDownloadableSubscriptionListResult(int result,
+ @Nullable DownloadableSubscription[] subscriptions) {
+ this.result = result;
+ if (this.result == EuiccService.RESULT_OK) {
+ this.mSubscriptions = subscriptions;
+ } else {
+ if (subscriptions != null) {
+ throw new IllegalArgumentException(
+ "Error result with non-null subscriptions: " + result);
+ }
+ this.mSubscriptions = null;
+ }
+ }
+
+ private GetDefaultDownloadableSubscriptionListResult(Parcel in) {
+ this.result = in.readInt();
+ this.mSubscriptions = in.createTypedArray(DownloadableSubscription.CREATOR);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(result);
+ dest.writeTypedArray(mSubscriptions, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/service/euicc/GetDownloadableSubscriptionMetadataResult.java b/android/service/euicc/GetDownloadableSubscriptionMetadataResult.java
new file mode 100644
index 0000000..d0fb511
--- /dev/null
+++ b/android/service/euicc/GetDownloadableSubscriptionMetadataResult.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017 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.euicc;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.euicc.DownloadableSubscription;
+
+/**
+ * Result of a {@link EuiccService#onGetDownloadableSubscriptionMetadata} operation.
+ * @hide
+ */
+@SystemApi
+public final class GetDownloadableSubscriptionMetadataResult implements Parcelable {
+
+ public static final @android.annotation.NonNull Creator<GetDownloadableSubscriptionMetadataResult> CREATOR =
+ new Creator<GetDownloadableSubscriptionMetadataResult>() {
+ @Override
+ public GetDownloadableSubscriptionMetadataResult createFromParcel(Parcel in) {
+ return new GetDownloadableSubscriptionMetadataResult(in);
+ }
+
+ @Override
+ public GetDownloadableSubscriptionMetadataResult[] newArray(int size) {
+ return new GetDownloadableSubscriptionMetadataResult[size];
+ }
+ };
+
+ /**
+ * @hide
+ * @deprecated - Do no use. Use getResult() instead.
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public final int result;
+
+ @Nullable
+ private final DownloadableSubscription mSubscription;
+
+ /**
+ * Gets the result of the operation.
+ *
+ * <p>May be one of the predefined {@code RESULT_} constants in EuiccService or any
+ * implementation-specific code starting with {@link EuiccService#RESULT_FIRST_USER}.
+ */
+ public int getResult() {
+ return result;
+ }
+
+ /**
+ * Gets the {@link DownloadableSubscription} with filled-in metadata.
+ *
+ * <p>Only non-null if {@link #result} is {@link EuiccService#RESULT_OK}.
+ */
+ @Nullable
+ public DownloadableSubscription getDownloadableSubscription() {
+ return mSubscription;
+ }
+
+ /**
+ * Construct a new {@link GetDownloadableSubscriptionMetadataResult}.
+ *
+ * @param result Result of the operation. May be one of the predefined {@code RESULT_} constants
+ * in EuiccService or any implementation-specific code starting with
+ * {@link EuiccService#RESULT_FIRST_USER}.
+ * @param subscription The subscription with filled-in metadata. Should only be provided if the
+ * result is {@link EuiccService#RESULT_OK}.
+ */
+ public GetDownloadableSubscriptionMetadataResult(int result,
+ @Nullable DownloadableSubscription subscription) {
+ this.result = result;
+ if (this.result == EuiccService.RESULT_OK) {
+ this.mSubscription = subscription;
+ } else {
+ if (subscription != null) {
+ throw new IllegalArgumentException(
+ "Error result with non-null subscription: " + result);
+ }
+ this.mSubscription = null;
+ }
+ }
+
+ private GetDownloadableSubscriptionMetadataResult(Parcel in) {
+ this.result = in.readInt();
+ this.mSubscription = in.readTypedObject(DownloadableSubscription.CREATOR);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(result);
+ dest.writeTypedObject(this.mSubscription, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/android/service/euicc/GetEuiccProfileInfoListResult.java b/android/service/euicc/GetEuiccProfileInfoListResult.java
new file mode 100644
index 0000000..9add38e
--- /dev/null
+++ b/android/service/euicc/GetEuiccProfileInfoListResult.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2017 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.euicc;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Result of a {@link EuiccService#onGetEuiccProfileInfoList} operation.
+ * @hide
+ */
+@SystemApi
+public final class GetEuiccProfileInfoListResult implements Parcelable {
+
+ public static final @android.annotation.NonNull Creator<GetEuiccProfileInfoListResult> CREATOR =
+ new Creator<GetEuiccProfileInfoListResult>() {
+ @Override
+ public GetEuiccProfileInfoListResult createFromParcel(Parcel in) {
+ return new GetEuiccProfileInfoListResult(in);
+ }
+
+ @Override
+ public GetEuiccProfileInfoListResult[] newArray(int size) {
+ return new GetEuiccProfileInfoListResult[size];
+ }
+ };
+
+ /**
+ * @hide
+ * @deprecated - Do no use. Use getResult() instead.
+ */
+ @Deprecated
+ public final int result;
+
+ @Nullable
+ private final EuiccProfileInfo[] mProfiles;
+
+ private final boolean mIsRemovable;
+
+ /**
+ * Gets the result of the operation.
+ *
+ * <p>May be one of the predefined {@code RESULT_} constants in EuiccService or any
+ * implementation-specific code starting with {@link EuiccService#RESULT_FIRST_USER}.
+ */
+ public int getResult() {
+ return result;
+ }
+
+ /** Gets the profile list (only upon success). */
+ @Nullable
+ public List<EuiccProfileInfo> getProfiles() {
+ if (mProfiles == null) return null;
+ return Arrays.asList(mProfiles);
+ }
+
+ /** Gets whether the eUICC is removable. */
+ public boolean getIsRemovable() {
+ return mIsRemovable;
+ }
+
+ /**
+ * Construct a new {@link GetEuiccProfileInfoListResult}.
+ *
+ * @param result Result of the operation. May be one of the predefined {@code RESULT_} constants
+ * in EuiccService or any implementation-specific code starting with
+ * {@link EuiccService#RESULT_FIRST_USER}.
+ * @param profiles the list of profiles. Should only be provided if the result is
+ * {@link EuiccService#RESULT_OK}.
+ * @param isRemovable whether the eUICC in this slot is removable. If true, the profiles
+ * returned here will only be considered accessible as long as this eUICC is present.
+ * Otherwise, they will remain accessible until the next time a response with isRemovable
+ * set to false is returned.
+ */
+ public GetEuiccProfileInfoListResult(
+ int result, @Nullable EuiccProfileInfo[] profiles, boolean isRemovable) {
+ this.result = result;
+ this.mIsRemovable = isRemovable;
+ if (this.result == EuiccService.RESULT_OK) {
+ this.mProfiles = profiles;
+ } else {
+ // For error case, profiles is either null or 0 size.
+ if (profiles != null && profiles.length > 0) {
+ throw new IllegalArgumentException(
+ "Error result with non-empty profiles: " + result);
+ }
+ this.mProfiles = null;
+ }
+ }
+
+ private GetEuiccProfileInfoListResult(Parcel in) {
+ this.result = in.readInt();
+ this.mProfiles = in.createTypedArray(EuiccProfileInfo.CREATOR);
+ this.mIsRemovable = in.readBoolean();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(result);
+ dest.writeTypedArray(mProfiles, flags);
+ dest.writeBoolean(mIsRemovable);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/service/gatekeeper/GateKeeperResponse.java b/android/service/gatekeeper/GateKeeperResponse.java
new file mode 100644
index 0000000..7ed733c
--- /dev/null
+++ b/android/service/gatekeeper/GateKeeperResponse.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2015 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.gatekeeper;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Response object for a GateKeeper verification request.
+ * @hide
+ */
+public final class GateKeeperResponse implements Parcelable {
+
+ public static final int RESPONSE_ERROR = -1;
+ public static final int RESPONSE_OK = 0;
+ public static final int RESPONSE_RETRY = 1;
+
+ public static final GateKeeperResponse ERROR = createGenericResponse(RESPONSE_ERROR);
+
+ private final int mResponseCode;
+
+ private int mTimeout;
+ private byte[] mPayload;
+ private boolean mShouldReEnroll;
+
+ /** Default constructor for response with generic response code **/
+ private GateKeeperResponse(int responseCode) {
+ mResponseCode = responseCode;
+ }
+
+ @VisibleForTesting
+ public static GateKeeperResponse createGenericResponse(int responseCode) {
+ return new GateKeeperResponse(responseCode);
+ }
+
+ private static GateKeeperResponse createRetryResponse(int timeout) {
+ GateKeeperResponse response = new GateKeeperResponse(RESPONSE_RETRY);
+ response.mTimeout = timeout;
+ return response;
+ }
+
+ @VisibleForTesting
+ public static GateKeeperResponse createOkResponse(byte[] payload, boolean shouldReEnroll) {
+ GateKeeperResponse response = new GateKeeperResponse(RESPONSE_OK);
+ response.mPayload = payload;
+ response.mShouldReEnroll = shouldReEnroll;
+ return response;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<GateKeeperResponse> CREATOR
+ = new Parcelable.Creator<GateKeeperResponse>() {
+ @Override
+ public GateKeeperResponse createFromParcel(Parcel source) {
+ int responseCode = source.readInt();
+ final GateKeeperResponse response;
+ if (responseCode == RESPONSE_RETRY) {
+ response = createRetryResponse(source.readInt());
+ } else if (responseCode == RESPONSE_OK) {
+ final boolean shouldReEnroll = source.readInt() == 1;
+ byte[] payload = null;
+ int size = source.readInt();
+ if (size > 0) {
+ payload = new byte[size];
+ source.readByteArray(payload);
+ }
+ response = createOkResponse(payload, shouldReEnroll);
+ } else {
+ response = createGenericResponse(responseCode);
+ }
+ return response;
+ }
+
+ @Override
+ public GateKeeperResponse[] newArray(int size) {
+ return new GateKeeperResponse[size];
+ }
+
+ };
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mResponseCode);
+ if (mResponseCode == RESPONSE_RETRY) {
+ dest.writeInt(mTimeout);
+ } else if (mResponseCode == RESPONSE_OK) {
+ dest.writeInt(mShouldReEnroll ? 1 : 0);
+ if (mPayload != null) {
+ dest.writeInt(mPayload.length);
+ dest.writeByteArray(mPayload);
+ } else {
+ dest.writeInt(0);
+ }
+ }
+ }
+
+ public byte[] getPayload() {
+ return mPayload;
+ }
+
+ public int getTimeout() {
+ return mTimeout;
+ }
+
+ public boolean getShouldReEnroll() {
+ return mShouldReEnroll;
+ }
+
+ public int getResponseCode() {
+ return mResponseCode;
+ }
+}
diff --git a/android/service/media/CameraPrewarmService.java b/android/service/media/CameraPrewarmService.java
new file mode 100644
index 0000000..335b00a
--- /dev/null
+++ b/android/service/media/CameraPrewarmService.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2015 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.media;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+
+/**
+ * Extend this class to implement a camera prewarm service. See
+ * {@link android.provider.MediaStore#META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE}.
+ */
+public abstract class CameraPrewarmService extends Service {
+
+ /**
+ * Intent action to bind the service as a prewarm service.
+ * @hide
+ */
+ public static final String ACTION_PREWARM =
+ "android.service.media.CameraPrewarmService.ACTION_PREWARM";
+
+ /**
+ * Message sent by the client indicating that the camera intent has been fired.
+ * @hide
+ */
+ public static final int MSG_CAMERA_FIRED = 1;
+
+ private final Handler mHandler = new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_CAMERA_FIRED:
+ mCameraIntentFired = true;
+ break;
+ default:
+ super.handleMessage(msg);
+ }
+ }
+ };
+ private boolean mCameraIntentFired;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (ACTION_PREWARM.equals(intent.getAction())) {
+ onPrewarm();
+ return new Messenger(mHandler).getBinder();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ if (ACTION_PREWARM.equals(intent.getAction())) {
+ onCooldown(mCameraIntentFired);
+ }
+ return false;
+ }
+
+ /**
+ * Called when the camera should be prewarmed.
+ */
+ public abstract void onPrewarm();
+
+ /**
+ * Called when prewarm phase is done, either because the camera launch intent has been fired
+ * at this point or prewarm is no longer needed. A client should close the camera
+ * immediately in the latter case.
+ * <p>
+ * In case the camera launch intent has been fired, there is no guarantee about the ordering
+ * of these two events. Cooldown might happen either before or after the activity has been
+ * created that handles the camera intent.
+ *
+ * @param cameraIntentFired Indicates whether the intent to launch the camera has been
+ * fired.
+ */
+ public abstract void onCooldown(boolean cameraIntentFired);
+}
diff --git a/android/service/media/MediaBrowserService.java b/android/service/media/MediaBrowserService.java
new file mode 100644
index 0000000..06adf30
--- /dev/null
+++ b/android/service/media/MediaBrowserService.java
@@ -0,0 +1,854 @@
+/*
+ * Copyright (C) 2014 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.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.app.Service;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.media.browse.MediaBrowser;
+import android.media.browse.MediaBrowserUtils;
+import android.media.session.MediaSession;
+import android.media.session.MediaSessionManager;
+import android.media.session.MediaSessionManager.RemoteUserInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Base class for media browser services.
+ * <p>
+ * Media browser services enable applications to browse media content provided by an application
+ * and ask the application to start playing it. They may also be used to control content that
+ * is already playing by way of a {@link MediaSession}.
+ * </p>
+ *
+ * To extend this class, you must declare the service in your manifest file with
+ * an intent filter with the {@link #SERVICE_INTERFACE} action.
+ *
+ * For example:
+ * </p><pre>
+ * <service android:name=".MyMediaBrowserService"
+ * android:label="@string/service_name" >
+ * <intent-filter>
+ * <action android:name="android.media.browse.MediaBrowserService" />
+ * </intent-filter>
+ * </service>
+ * </pre>
+ *
+ */
+public abstract class MediaBrowserService extends Service {
+ private static final String TAG = "MediaBrowserService";
+ private static final boolean DBG = false;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
+
+ /**
+ * A key for passing the MediaItem to the ResultReceiver in getItem.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static final String KEY_MEDIA_ITEM = "media_item";
+
+ private static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0;
+ private static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1;
+
+ private static final int RESULT_ERROR = -1;
+ private static final int RESULT_OK = 0;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
+ RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED })
+ private @interface ResultFlags { }
+
+ private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
+ private ConnectionRecord mCurConnection;
+ private final Handler mHandler = new Handler();
+ private ServiceBinder mBinder;
+ MediaSession.Token mSession;
+
+ /**
+ * All the info about a connection.
+ */
+ private class ConnectionRecord implements IBinder.DeathRecipient {
+ String pkg;
+ int uid;
+ int pid;
+ Bundle rootHints;
+ IMediaBrowserServiceCallbacks callbacks;
+ BrowserRoot root;
+ HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>();
+
+ @Override
+ public void binderDied() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mConnections.remove(callbacks.asBinder());
+ }
+ });
+ }
+ }
+
+ /**
+ * Completion handler for asynchronous callback methods in {@link MediaBrowserService}.
+ * <p>
+ * Each of the methods that takes one of these to send the result must call
+ * {@link #sendResult} to respond to the caller with the given results. If those
+ * functions return without calling {@link #sendResult}, they must instead call
+ * {@link #detach} before returning, and then may call {@link #sendResult} when
+ * they are done. If more than one of those methods is called, an exception will
+ * be thrown.
+ *
+ * @see #onLoadChildren
+ * @see #onLoadItem
+ */
+ public class Result<T> {
+ private Object mDebug;
+ private boolean mDetachCalled;
+ private boolean mSendResultCalled;
+ @UnsupportedAppUsage
+ private int mFlags;
+
+ Result(Object debug) {
+ mDebug = debug;
+ }
+
+ /**
+ * Send the result back to the caller.
+ */
+ public void sendResult(T result) {
+ if (mSendResultCalled) {
+ throw new IllegalStateException("sendResult() called twice for: " + mDebug);
+ }
+ mSendResultCalled = true;
+ onResultSent(result, mFlags);
+ }
+
+ /**
+ * Detach this message from the current thread and allow the {@link #sendResult}
+ * call to happen later.
+ */
+ public void detach() {
+ if (mDetachCalled) {
+ throw new IllegalStateException("detach() called when detach() had already"
+ + " been called for: " + mDebug);
+ }
+ if (mSendResultCalled) {
+ throw new IllegalStateException("detach() called when sendResult() had already"
+ + " been called for: " + mDebug);
+ }
+ mDetachCalled = true;
+ }
+
+ boolean isDone() {
+ return mDetachCalled || mSendResultCalled;
+ }
+
+ void setFlags(@ResultFlags int flags) {
+ mFlags = flags;
+ }
+
+ /**
+ * Called when the result is sent, after assertions about not being called twice
+ * have happened.
+ */
+ void onResultSent(T result, @ResultFlags int flags) {
+ }
+ }
+
+ private class ServiceBinder extends IMediaBrowserService.Stub {
+ @Override
+ public void connect(final String pkg, final Bundle rootHints,
+ final IMediaBrowserServiceCallbacks callbacks) {
+
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ if (!isValidPackage(pkg, uid)) {
+ throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
+ + " package=" + pkg);
+ }
+
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ // Clear out the old subscriptions. We are getting new ones.
+ mConnections.remove(b);
+
+ final ConnectionRecord connection = new ConnectionRecord();
+ connection.pkg = pkg;
+ connection.pid = pid;
+ connection.uid = uid;
+ connection.rootHints = rootHints;
+ connection.callbacks = callbacks;
+
+ mCurConnection = connection;
+ connection.root = MediaBrowserService.this.onGetRoot(pkg, uid, rootHints);
+ mCurConnection = null;
+
+ // If they didn't return something, don't allow this client.
+ if (connection.root == null) {
+ Log.i(TAG, "No root for client " + pkg + " from service "
+ + getClass().getName());
+ try {
+ callbacks.onConnectFailed();
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
+ + "pkg=" + pkg);
+ }
+ } else {
+ try {
+ mConnections.put(b, connection);
+ b.linkToDeath(connection, 0);
+ if (mSession != null) {
+ callbacks.onConnect(connection.root.getRootId(),
+ mSession, connection.root.getExtras());
+ }
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Calling onConnect() failed. Dropping client. "
+ + "pkg=" + pkg);
+ mConnections.remove(b);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void disconnect(final IMediaBrowserServiceCallbacks callbacks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ // Clear out the old subscriptions. We are getting new ones.
+ final ConnectionRecord old = mConnections.remove(b);
+ if (old != null) {
+ // TODO
+ old.callbacks.asBinder().unlinkToDeath(old, 0);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void addSubscriptionDeprecated(String id, IMediaBrowserServiceCallbacks callbacks) {
+ // do-nothing
+ }
+
+ @Override
+ public void addSubscription(final String id, final IBinder token, final Bundle options,
+ final IMediaBrowserServiceCallbacks callbacks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ // Get the record for the connection
+ final ConnectionRecord connection = mConnections.get(b);
+ if (connection == null) {
+ Log.w(TAG, "addSubscription for callback that isn't registered id="
+ + id);
+ return;
+ }
+
+ MediaBrowserService.this.addSubscription(id, connection, token, options);
+ }
+ });
+ }
+
+ @Override
+ public void removeSubscriptionDeprecated(
+ String id, IMediaBrowserServiceCallbacks callbacks) {
+ // do-nothing
+ }
+
+ @Override
+ public void removeSubscription(final String id, final IBinder token,
+ final IMediaBrowserServiceCallbacks callbacks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+
+ ConnectionRecord connection = mConnections.get(b);
+ if (connection == null) {
+ Log.w(TAG, "removeSubscription for callback that isn't registered id="
+ + id);
+ return;
+ }
+ if (!MediaBrowserService.this.removeSubscription(id, connection, token)) {
+ Log.w(TAG, "removeSubscription called for " + id
+ + " which is not subscribed");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void getMediaItem(final String mediaId, final ResultReceiver receiver,
+ final IMediaBrowserServiceCallbacks callbacks) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final IBinder b = callbacks.asBinder();
+ ConnectionRecord connection = mConnections.get(b);
+ if (connection == null) {
+ Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId);
+ return;
+ }
+ performLoadItem(mediaId, connection, receiver);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mBinder = new ServiceBinder();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mBinder;
+ }
+ return null;
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ }
+
+ /**
+ * Called to get the root information for browsing by a particular client.
+ * <p>
+ * The implementation should verify that the client package has permission
+ * to access browse media information before returning the root id; it
+ * should return null if the client is not allowed to access this
+ * information.
+ * </p>
+ *
+ * @param clientPackageName The package name of the application which is
+ * requesting access to browse media.
+ * @param clientUid The uid of the application which is requesting access to
+ * browse media.
+ * @param rootHints An optional bundle of service-specific arguments to send
+ * to the media browser service when connecting and retrieving the
+ * root id for browsing, or null if none. The contents of this
+ * bundle may affect the information returned when browsing.
+ * @return The {@link BrowserRoot} for accessing this app's content or null.
+ * @see BrowserRoot#EXTRA_RECENT
+ * @see BrowserRoot#EXTRA_OFFLINE
+ * @see BrowserRoot#EXTRA_SUGGESTED
+ */
+ public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
+ int clientUid, @Nullable Bundle rootHints);
+
+ /**
+ * Called to get information about the children of a media item.
+ * <p>
+ * Implementations must call {@link Result#sendResult result.sendResult}
+ * with the list of children. If loading the children will be an expensive
+ * operation that should be performed on another thread,
+ * {@link Result#detach result.detach} may be called before returning from
+ * this function, and then {@link Result#sendResult result.sendResult}
+ * called when the loading is complete.
+ * </p><p>
+ * In case the media item does not have any children, call {@link Result#sendResult}
+ * with an empty list. When the given {@code parentId} is invalid, implementations must
+ * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
+ * {@link MediaBrowser.SubscriptionCallback#onError}.
+ * </p>
+ *
+ * @param parentId The id of the parent media item whose children are to be
+ * queried.
+ * @param result The Result to send the list of children to.
+ */
+ public abstract void onLoadChildren(@NonNull String parentId,
+ @NonNull Result<List<MediaBrowser.MediaItem>> result);
+
+ /**
+ * Called to get information about the children of a media item.
+ * <p>
+ * Implementations must call {@link Result#sendResult result.sendResult}
+ * with the list of children. If loading the children will be an expensive
+ * operation that should be performed on another thread,
+ * {@link Result#detach result.detach} may be called before returning from
+ * this function, and then {@link Result#sendResult result.sendResult}
+ * called when the loading is complete.
+ * </p><p>
+ * In case the media item does not have any children, call {@link Result#sendResult}
+ * with an empty list. When the given {@code parentId} is invalid, implementations must
+ * call {@link Result#sendResult result.sendResult} with {@code null}, which will invoke
+ * {@link MediaBrowser.SubscriptionCallback#onError}.
+ * </p>
+ *
+ * @param parentId The id of the parent media item whose children are to be
+ * queried.
+ * @param result The Result to send the list of children to.
+ * @param options The bundle of service-specific arguments sent from the media
+ * browser. The information returned through the result should be
+ * affected by the contents of this bundle.
+ */
+ public void onLoadChildren(@NonNull String parentId,
+ @NonNull Result<List<MediaBrowser.MediaItem>> result, @NonNull Bundle options) {
+ // To support backward compatibility, when the implementation of MediaBrowserService doesn't
+ // override onLoadChildren() with options, onLoadChildren() without options will be used
+ // instead, and the options will be applied in the implementation of result.onResultSent().
+ result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED);
+ onLoadChildren(parentId, result);
+ }
+
+ /**
+ * Called to get information about a specific media item.
+ * <p>
+ * Implementations must call {@link Result#sendResult result.sendResult}. If
+ * loading the item will be an expensive operation {@link Result#detach
+ * result.detach} may be called before returning from this function, and
+ * then {@link Result#sendResult result.sendResult} called when the item has
+ * been loaded.
+ * </p><p>
+ * When the given {@code itemId} is invalid, implementations must call
+ * {@link Result#sendResult result.sendResult} with {@code null}.
+ * </p><p>
+ * The default implementation will invoke {@link MediaBrowser.ItemCallback#onError}.
+ * </p>
+ *
+ * @param itemId The id for the specific
+ * {@link android.media.browse.MediaBrowser.MediaItem}.
+ * @param result The Result to send the item to.
+ */
+ public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
+ result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED);
+ result.sendResult(null);
+ }
+
+ /**
+ * Call to set the media session.
+ * <p>
+ * This should be called as soon as possible during the service's startup.
+ * It may only be called once.
+ *
+ * @param token The token for the service's {@link MediaSession}.
+ */
+ public void setSessionToken(final MediaSession.Token token) {
+ if (token == null) {
+ throw new IllegalArgumentException("Session token may not be null.");
+ }
+ if (mSession != null) {
+ throw new IllegalStateException("The session token has already been set.");
+ }
+ mSession = token;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Iterator<ConnectionRecord> iter = mConnections.values().iterator();
+ while (iter.hasNext()) {
+ ConnectionRecord connection = iter.next();
+ try {
+ connection.callbacks.onConnect(connection.root.getRootId(), token,
+ connection.root.getExtras());
+ } catch (RemoteException e) {
+ Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid.");
+ iter.remove();
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Gets the session token, or null if it has not yet been created
+ * or if it has been destroyed.
+ */
+ public @Nullable MediaSession.Token getSessionToken() {
+ return mSession;
+ }
+
+ /**
+ * Gets the root hints sent from the currently connected {@link MediaBrowser}.
+ * The root hints are service-specific arguments included in an optional bundle sent to the
+ * media browser service when connecting and retrieving the root id for browsing, or null if
+ * none. The contents of this bundle may affect the information returned when browsing.
+ *
+ * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
+ * {@link #onLoadChildren} or {@link #onLoadItem}.
+ * @see MediaBrowserService.BrowserRoot#EXTRA_RECENT
+ * @see MediaBrowserService.BrowserRoot#EXTRA_OFFLINE
+ * @see MediaBrowserService.BrowserRoot#EXTRA_SUGGESTED
+ */
+ public final Bundle getBrowserRootHints() {
+ if (mCurConnection == null) {
+ throw new IllegalStateException("This should be called inside of onGetRoot or"
+ + " onLoadChildren or onLoadItem methods");
+ }
+ return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
+ }
+
+ /**
+ * Gets the browser information who sent the current request.
+ *
+ * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
+ * {@link #onLoadChildren} or {@link #onLoadItem}.
+ * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
+ */
+ public final RemoteUserInfo getCurrentBrowserInfo() {
+ if (mCurConnection == null) {
+ throw new IllegalStateException("This should be called inside of onGetRoot or"
+ + " onLoadChildren or onLoadItem methods");
+ }
+ return new RemoteUserInfo(mCurConnection.pkg, mCurConnection.pid, mCurConnection.uid);
+ }
+
+ /**
+ * Notifies all connected media browsers that the children of
+ * the specified parent id have changed in some way.
+ * This will cause browsers to fetch subscribed content again.
+ *
+ * @param parentId The id of the parent media item whose
+ * children changed.
+ */
+ public void notifyChildrenChanged(@NonNull String parentId) {
+ notifyChildrenChangedInternal(parentId, null);
+ }
+
+ /**
+ * Notifies all connected media browsers that the children of
+ * the specified parent id have changed in some way.
+ * This will cause browsers to fetch subscribed content again.
+ *
+ * @param parentId The id of the parent media item whose
+ * children changed.
+ * @param options The bundle of service-specific arguments to send
+ * to the media browser. The contents of this bundle may
+ * contain the information about the change.
+ */
+ public void notifyChildrenChanged(@NonNull String parentId, @NonNull Bundle options) {
+ if (options == null) {
+ throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged");
+ }
+ notifyChildrenChangedInternal(parentId, options);
+ }
+
+ private void notifyChildrenChangedInternal(final String parentId, final Bundle options) {
+ if (parentId == null) {
+ throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged");
+ }
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (IBinder binder : mConnections.keySet()) {
+ ConnectionRecord connection = mConnections.get(binder);
+ List<Pair<IBinder, Bundle>> callbackList =
+ connection.subscriptions.get(parentId);
+ if (callbackList != null) {
+ for (Pair<IBinder, Bundle> callback : callbackList) {
+ if (MediaBrowserUtils.hasDuplicatedItems(options, callback.second)) {
+ performLoadChildren(parentId, connection, callback.second);
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Return whether the given package is one of the ones that is owned by the uid.
+ */
+ private boolean isValidPackage(String pkg, int uid) {
+ if (pkg == null) {
+ return false;
+ }
+ final PackageManager pm = getPackageManager();
+ final String[] packages = pm.getPackagesForUid(uid);
+ final int N = packages.length;
+ for (int i = 0; i < N; i++) {
+ if (packages[i].equals(pkg)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Save the subscription and if it is a new subscription send the results.
+ */
+ private void addSubscription(String id, ConnectionRecord connection, IBinder token,
+ Bundle options) {
+ // Save the subscription
+ List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
+ if (callbackList == null) {
+ callbackList = new ArrayList<>();
+ }
+ for (Pair<IBinder, Bundle> callback : callbackList) {
+ if (token == callback.first
+ && MediaBrowserUtils.areSameOptions(options, callback.second)) {
+ return;
+ }
+ }
+ callbackList.add(new Pair<>(token, options));
+ connection.subscriptions.put(id, callbackList);
+ // send the results
+ performLoadChildren(id, connection, options);
+ }
+
+ /**
+ * Remove the subscription.
+ */
+ private boolean removeSubscription(String id, ConnectionRecord connection, IBinder token) {
+ if (token == null) {
+ return connection.subscriptions.remove(id) != null;
+ }
+ boolean removed = false;
+ List<Pair<IBinder, Bundle>> callbackList = connection.subscriptions.get(id);
+ if (callbackList != null) {
+ Iterator<Pair<IBinder, Bundle>> iter = callbackList.iterator();
+ while (iter.hasNext()) {
+ if (token == iter.next().first) {
+ removed = true;
+ iter.remove();
+ }
+ }
+ if (callbackList.size() == 0) {
+ connection.subscriptions.remove(id);
+ }
+ }
+ return removed;
+ }
+
+ /**
+ * Call onLoadChildren and then send the results back to the connection.
+ * <p>
+ * Callers must make sure that this connection is still connected.
+ */
+ private void performLoadChildren(final String parentId, final ConnectionRecord connection,
+ final Bundle options) {
+ final Result<List<MediaBrowser.MediaItem>> result =
+ new Result<List<MediaBrowser.MediaItem>>(parentId) {
+ @Override
+ void onResultSent(List<MediaBrowser.MediaItem> list, @ResultFlags int flag) {
+ if (mConnections.get(connection.callbacks.asBinder()) != connection) {
+ if (DBG) {
+ Log.d(TAG, "Not sending onLoadChildren result for connection that has"
+ + " been disconnected. pkg=" + connection.pkg + " id=" + parentId);
+ }
+ return;
+ }
+
+ List<MediaBrowser.MediaItem> filteredList =
+ (flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
+ ? applyOptions(list, options) : list;
+ final ParceledListSlice<MediaBrowser.MediaItem> pls =
+ filteredList == null ? null : new ParceledListSlice<>(filteredList);
+ try {
+ connection.callbacks.onLoadChildrenWithOptions(parentId, pls, options);
+ } catch (RemoteException ex) {
+ // The other side is in the process of crashing.
+ Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
+ + " package=" + connection.pkg);
+ }
+ }
+ };
+
+ mCurConnection = connection;
+ if (options == null) {
+ onLoadChildren(parentId, result);
+ } else {
+ onLoadChildren(parentId, result, options);
+ }
+ mCurConnection = null;
+
+ if (!result.isDone()) {
+ throw new IllegalStateException("onLoadChildren must call detach() or sendResult()"
+ + " before returning for package=" + connection.pkg + " id=" + parentId);
+ }
+ }
+
+ private List<MediaBrowser.MediaItem> applyOptions(List<MediaBrowser.MediaItem> list,
+ final Bundle options) {
+ if (list == null) {
+ return null;
+ }
+ int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1);
+ int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1);
+ if (page == -1 && pageSize == -1) {
+ return list;
+ }
+ int fromIndex = pageSize * page;
+ int toIndex = fromIndex + pageSize;
+ if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
+ return Collections.EMPTY_LIST;
+ }
+ if (toIndex > list.size()) {
+ toIndex = list.size();
+ }
+ return list.subList(fromIndex, toIndex);
+ }
+
+ private void performLoadItem(String itemId, final ConnectionRecord connection,
+ final ResultReceiver receiver) {
+ final Result<MediaBrowser.MediaItem> result =
+ new Result<MediaBrowser.MediaItem>(itemId) {
+ @Override
+ void onResultSent(MediaBrowser.MediaItem item, @ResultFlags int flag) {
+ if (mConnections.get(connection.callbacks.asBinder()) != connection) {
+ if (DBG) {
+ Log.d(TAG, "Not sending onLoadItem result for connection that has"
+ + " been disconnected. pkg=" + connection.pkg + " id=" + itemId);
+ }
+ return;
+ }
+ if ((flag & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) {
+ receiver.send(RESULT_ERROR, null);
+ return;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(KEY_MEDIA_ITEM, item);
+ receiver.send(RESULT_OK, bundle);
+ }
+ };
+
+ mCurConnection = connection;
+ onLoadItem(itemId, result);
+ mCurConnection = null;
+
+ if (!result.isDone()) {
+ throw new IllegalStateException("onLoadItem must call detach() or sendResult()"
+ + " before returning for id=" + itemId);
+ }
+ }
+
+ /**
+ * Contains information that the browser service needs to send to the client
+ * when first connected.
+ */
+ public static final class BrowserRoot {
+ /**
+ * The lookup key for a boolean that indicates whether the browser service should return a
+ * browser root for recently played media items.
+ *
+ * <p>When creating a media browser for a given media browser service, this key can be
+ * supplied as a root hint for retrieving media items that are recently played.
+ * If the media browser service can provide such media items, the implementation must return
+ * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_OFFLINE
+ * @see #EXTRA_SUGGESTED
+ */
+ public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
+
+ /**
+ * The lookup key for a boolean that indicates whether the browser service should return a
+ * browser root for offline media items.
+ *
+ * <p>When creating a media browser for a given media browser service, this key can be
+ * supplied as a root hint for retrieving media items that are can be played without an
+ * internet connection.
+ * If the media browser service can provide such media items, the implementation must return
+ * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_RECENT
+ * @see #EXTRA_SUGGESTED
+ */
+ public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
+
+ /**
+ * The lookup key for a boolean that indicates whether the browser service should return a
+ * browser root for suggested media items.
+ *
+ * <p>When creating a media browser for a given media browser service, this key can be
+ * supplied as a root hint for retrieving the media items suggested by the media browser
+ * service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
+ * is considered ordered by relevance, first being the top suggestion.
+ * If the media browser service can provide such media items, the implementation must return
+ * the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_RECENT
+ * @see #EXTRA_OFFLINE
+ */
+ public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
+
+ private final String mRootId;
+ private final Bundle mExtras;
+
+ /**
+ * Constructs a browser root.
+ * @param rootId The root id for browsing.
+ * @param extras Any extras about the browser service.
+ */
+ public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
+ if (rootId == null) {
+ throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. "
+ + "Use null for BrowserRoot instead.");
+ }
+ mRootId = rootId;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the root id for browsing.
+ */
+ public String getRootId() {
+ return mRootId;
+ }
+
+ /**
+ * Gets any extras about the browser service.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+ }
+}
diff --git a/android/service/notification/Adjustment.java b/android/service/notification/Adjustment.java
new file mode 100644
index 0000000..8464c6d
--- /dev/null
+++ b/android/service/notification/Adjustment.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Notification;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Ranking updates from the Assistant.
+ *
+ * The updates are provides as a {@link Bundle} of signals, using the keys provided in this
+ * class.
+ * Each {@code KEY} specifies what type of data it supports and what kind of Adjustment it
+ * realizes on the notification rankings.
+ *
+ * Notifications affected by the Adjustment will be re-ranked if necessary.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class Adjustment implements Parcelable {
+ private final String mPackage;
+ private final String mKey;
+ private final CharSequence mExplanation;
+ private final Bundle mSignals;
+ private final int mUser;
+ @Nullable private String mIssuer;
+
+ /** @hide */
+ @StringDef (prefix = { "KEY_" }, value = {
+ KEY_CONTEXTUAL_ACTIONS, KEY_GROUP_KEY, KEY_IMPORTANCE, KEY_PEOPLE, KEY_SNOOZE_CRITERIA,
+ KEY_TEXT_REPLIES, KEY_USER_SENTIMENT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Keys {}
+
+ /**
+ * Data type: ArrayList of {@code String}, where each is a representation of a
+ * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+ * See {@link android.app.Notification.Builder#addPerson(String)}.
+ * @hide
+ */
+ @SystemApi
+ public static final String KEY_PEOPLE = "key_people";
+ /**
+ * Parcelable {@code ArrayList} of {@link SnoozeCriterion}. These criteria may be visible to
+ * users. If a user chooses to snooze a notification until one of these criterion, the
+ * assistant will be notified via
+ * {@link NotificationAssistantService#onNotificationSnoozedUntilContext}.
+ */
+ public static final String KEY_SNOOZE_CRITERIA = "key_snooze_criteria";
+ /**
+ * Data type: String. Used to change what {@link Notification#getGroup() group} a notification
+ * belongs to.
+ * @hide
+ */
+ public static final String KEY_GROUP_KEY = "key_group_key";
+
+ /**
+ * Data type: int, one of {@link NotificationListenerService.Ranking#USER_SENTIMENT_POSITIVE},
+ * {@link NotificationListenerService.Ranking#USER_SENTIMENT_NEUTRAL},
+ * {@link NotificationListenerService.Ranking#USER_SENTIMENT_NEGATIVE}. Used to express how
+ * a user feels about notifications in the same {@link android.app.NotificationChannel} as
+ * the notification represented by {@link #getKey()}.
+ */
+ public static final String KEY_USER_SENTIMENT = "key_user_sentiment";
+
+ /**
+ * Data type: ArrayList of {@link android.app.Notification.Action}.
+ * Used to suggest contextual actions for a notification.
+ *
+ * @see Notification.Action.Builder#setContextual(boolean)
+ */
+ public static final String KEY_CONTEXTUAL_ACTIONS = "key_contextual_actions";
+
+ /**
+ * Data type: ArrayList of {@link CharSequence}.
+ * Used to suggest smart replies for a notification.
+ */
+ public static final String KEY_TEXT_REPLIES = "key_text_replies";
+
+ /**
+ * Data type: int, one of importance values e.g.
+ * {@link android.app.NotificationManager#IMPORTANCE_MIN}.
+ *
+ * <p> If used from
+ * {@link NotificationAssistantService#onNotificationEnqueued(StatusBarNotification)}, and
+ * received before the notification is posted, it can block a notification from appearing or
+ * silence it. Importance adjustments received too late from
+ * {@link NotificationAssistantService#onNotificationEnqueued(StatusBarNotification)} will be
+ * ignored.
+ * </p>
+ * <p>If used from
+ * {@link NotificationAssistantService#adjustNotification(Adjustment)}, it can
+ * visually demote or cancel a notification, but use this with care if they notification was
+ * recently posted because the notification may already have made noise.
+ * </p>
+ */
+ public static final String KEY_IMPORTANCE = "key_importance";
+
+ /**
+ * Data type: float, a ranking score from 0 (lowest) to 1 (highest).
+ * Used to rank notifications inside that fall under the same classification (i.e. alerting,
+ * silenced).
+ */
+ public static final String KEY_RANKING_SCORE = "key_ranking_score";
+
+ /**
+ * Data type: boolean, when true it suggests this is NOT a conversation notification.
+ * @hide
+ */
+ @SystemApi
+ public static final String KEY_NOT_CONVERSATION = "key_not_conversation";
+
+ /**
+ * Create a notification adjustment.
+ *
+ * @param pkg The package of the notification.
+ * @param key The notification key.
+ * @param signals A bundle of signals that should inform notification display, ordering, and
+ * interruptiveness.
+ * @param explanation A human-readable justification for the adjustment.
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public Adjustment(String pkg, String key, Bundle signals, CharSequence explanation, int user) {
+ mPackage = pkg;
+ mKey = key;
+ mSignals = signals;
+ mExplanation = explanation;
+ mUser = user;
+ }
+
+ /**
+ * Create a notification adjustment.
+ *
+ * @param pkg The package of the notification.
+ * @param key The notification key.
+ * @param signals A bundle of signals that should inform notification display, ordering, and
+ * interruptiveness.
+ * @param explanation A human-readable justification for the adjustment.
+ * @param userHandle User handle for for whose the adjustments will be applied.
+ */
+ public Adjustment(@NonNull String pkg, @NonNull String key, @NonNull Bundle signals,
+ @NonNull CharSequence explanation,
+ @NonNull UserHandle userHandle) {
+ mPackage = pkg;
+ mKey = key;
+ mSignals = signals;
+ mExplanation = explanation;
+ mUser = userHandle.getIdentifier();
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ protected Adjustment(Parcel in) {
+ if (in.readInt() == 1) {
+ mPackage = in.readString();
+ } else {
+ mPackage = null;
+ }
+ if (in.readInt() == 1) {
+ mKey = in.readString();
+ } else {
+ mKey = null;
+ }
+ if (in.readInt() == 1) {
+ mExplanation = in.readCharSequence();
+ } else {
+ mExplanation = null;
+ }
+ mSignals = in.readBundle();
+ mUser = in.readInt();
+ mIssuer = in.readString();
+ }
+
+ public static final @android.annotation.NonNull Creator<Adjustment> CREATOR = new Creator<Adjustment>() {
+ @Override
+ public Adjustment createFromParcel(Parcel in) {
+ return new Adjustment(in);
+ }
+
+ @Override
+ public Adjustment[] newArray(int size) {
+ return new Adjustment[size];
+ }
+ };
+
+ public @NonNull String getPackage() {
+ return mPackage;
+ }
+
+ public @NonNull String getKey() {
+ return mKey;
+ }
+
+ public @NonNull CharSequence getExplanation() {
+ return mExplanation;
+ }
+
+ public @NonNull Bundle getSignals() {
+ return mSignals;
+ }
+
+ /** @hide */
+ @SystemApi
+ @TestApi
+ public int getUser() {
+ return mUser;
+ }
+
+ public @NonNull UserHandle getUserHandle() {
+ return UserHandle.of(mUser);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mPackage != null) {
+ dest.writeInt(1);
+ dest.writeString(mPackage);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mKey != null) {
+ dest.writeInt(1);
+ dest.writeString(mKey);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mExplanation != null) {
+ dest.writeInt(1);
+ dest.writeCharSequence(mExplanation);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeBundle(mSignals);
+ dest.writeInt(mUser);
+ dest.writeString(mIssuer);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "Adjustment{"
+ + "mSignals=" + mSignals
+ + '}';
+ }
+
+ /** @hide */
+ public void setIssuer(@Nullable String issuer) {
+ mIssuer = issuer;
+ }
+
+ /** @hide */
+ public @Nullable String getIssuer() {
+ return mIssuer;
+ }
+}
diff --git a/android/service/notification/Condition.java b/android/service/notification/Condition.java
new file mode 100644
index 0000000..cf57e25
--- /dev/null
+++ b/android/service/notification/Condition.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright (c) 2014, 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.notification;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.proto.ProtoOutputStream;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * The current condition of an {@link android.app.AutomaticZenRule}, provided by the
+ * app that owns the rule. Used to tell the system to enter Do Not
+ * Disturb mode and request that the system exit Do Not Disturb mode.
+ */
+public final class Condition implements Parcelable {
+
+ public static final String SCHEME = "condition";
+
+ /** @hide */
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_FALSE,
+ STATE_TRUE,
+ STATE_UNKNOWN,
+ STATE_ERROR
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface State {}
+
+ /**
+ * Indicates that Do Not Disturb should be turned off. Note that all Conditions from all
+ * {@link android.app.AutomaticZenRule} providers must be off for Do Not Disturb to be turned
+ * off on the device.
+ */
+ public static final int STATE_FALSE = 0;
+ /**
+ * Indicates that Do Not Disturb should be turned on.
+ */
+ public static final int STATE_TRUE = 1;
+
+ public static final int STATE_UNKNOWN = 2;
+ public static final int STATE_ERROR = 3;
+
+ public static final int FLAG_RELEVANT_NOW = 1 << 0;
+ public static final int FLAG_RELEVANT_ALWAYS = 1 << 1;
+
+ /**
+ * The URI representing the rule being updated.
+ * See {@link android.app.AutomaticZenRule#getConditionId()}.
+ */
+ public final Uri id;
+
+ /**
+ * A summary of what the rule encoded in {@link #id} means when it is enabled. User visible
+ * if the state of the condition is {@link #STATE_TRUE}.
+ */
+ public final String summary;
+
+ public final String line1;
+ public final String line2;
+
+ /**
+ * The state of this condition. {@link #STATE_TRUE} will enable Do Not Disturb mode.
+ * {@link #STATE_FALSE} will turn Do Not Disturb off for this rule. Note that Do Not Disturb
+ * might still be enabled globally if other conditions are in a {@link #STATE_TRUE} state.
+ */
+ @State
+ public final int state;
+
+ public final int flags;
+ public final int icon;
+
+ /**
+ * An object representing the current state of a {@link android.app.AutomaticZenRule}.
+ * @param id the {@link android.app.AutomaticZenRule#getConditionId()} of the zen rule
+ * @param summary a user visible description of the rule state.
+ */
+ public Condition(Uri id, String summary, int state) {
+ this(id, summary, "", "", -1, state, FLAG_RELEVANT_ALWAYS);
+ }
+
+ public Condition(Uri id, String summary, String line1, String line2, int icon,
+ int state, int flags) {
+ if (id == null) throw new IllegalArgumentException("id is required");
+ if (summary == null) throw new IllegalArgumentException("summary is required");
+ if (!isValidState(state)) throw new IllegalArgumentException("state is invalid: " + state);
+ this.id = id;
+ this.summary = summary;
+ this.line1 = line1;
+ this.line2 = line2;
+ this.icon = icon;
+ this.state = state;
+ this.flags = flags;
+ }
+
+ public Condition(Parcel source) {
+ this((Uri)source.readParcelable(Condition.class.getClassLoader()),
+ source.readString(),
+ source.readString(),
+ source.readString(),
+ source.readInt(),
+ source.readInt(),
+ source.readInt());
+ }
+
+ private static boolean isValidState(int state) {
+ return state >= STATE_FALSE && state <= STATE_ERROR;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(id, 0);
+ dest.writeString(summary);
+ dest.writeString(line1);
+ dest.writeString(line2);
+ dest.writeInt(icon);
+ dest.writeInt(state);
+ dest.writeInt(this.flags);
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(Condition.class.getSimpleName()).append('[')
+ .append("state=").append(stateToString(state))
+ .append(",id=").append(id)
+ .append(",summary=").append(summary)
+ .append(",line1=").append(line1)
+ .append(",line2=").append(line2)
+ .append(",icon=").append(icon)
+ .append(",flags=").append(flags)
+ .append(']').toString();
+ }
+
+ /** @hide */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ // id is guaranteed not to be null.
+ proto.write(ConditionProto.ID, id.toString());
+ proto.write(ConditionProto.SUMMARY, summary);
+ proto.write(ConditionProto.LINE_1, line1);
+ proto.write(ConditionProto.LINE_2, line2);
+ proto.write(ConditionProto.ICON, icon);
+ proto.write(ConditionProto.STATE, state);
+ proto.write(ConditionProto.FLAGS, flags);
+
+ proto.end(token);
+ }
+
+ public static String stateToString(int state) {
+ if (state == STATE_FALSE) return "STATE_FALSE";
+ if (state == STATE_TRUE) return "STATE_TRUE";
+ if (state == STATE_UNKNOWN) return "STATE_UNKNOWN";
+ if (state == STATE_ERROR) return "STATE_ERROR";
+ throw new IllegalArgumentException("state is invalid: " + state);
+ }
+
+ public static String relevanceToString(int flags) {
+ final boolean now = (flags & FLAG_RELEVANT_NOW) != 0;
+ final boolean always = (flags & FLAG_RELEVANT_ALWAYS) != 0;
+ if (!now && !always) return "NONE";
+ if (now && always) return "NOW, ALWAYS";
+ return now ? "NOW" : "ALWAYS";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Condition)) return false;
+ if (o == this) return true;
+ final Condition other = (Condition) o;
+ return Objects.equals(other.id, id)
+ && Objects.equals(other.summary, summary)
+ && Objects.equals(other.line1, line1)
+ && Objects.equals(other.line2, line2)
+ && other.icon == icon
+ && other.state == state
+ && other.flags == flags;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, summary, line1, line2, icon, state, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public Condition copy() {
+ final Parcel parcel = Parcel.obtain();
+ try {
+ writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return new Condition(parcel);
+ } finally {
+ parcel.recycle();
+ }
+ }
+
+ public static Uri.Builder newId(Context context) {
+ return new Uri.Builder()
+ .scheme(Condition.SCHEME)
+ .authority(context.getPackageName());
+ }
+
+ public static boolean isValidId(Uri id, String pkg) {
+ return id != null && SCHEME.equals(id.getScheme()) && pkg.equals(id.getAuthority());
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<Condition> CREATOR
+ = new Parcelable.Creator<Condition>() {
+ @Override
+ public Condition createFromParcel(Parcel source) {
+ return new Condition(source);
+ }
+
+ @Override
+ public Condition[] newArray(int size) {
+ return new Condition[size];
+ }
+ };
+}
diff --git a/android/service/notification/ConditionProviderService.java b/android/service/notification/ConditionProviderService.java
new file mode 100644
index 0000000..f37e01d
--- /dev/null
+++ b/android/service/notification/ConditionProviderService.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2014 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.notification;
+
+import android.annotation.SdkConstant;
+import android.annotation.TestApi;
+import android.app.ActivityManager;
+import android.app.INotificationManager;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+/**
+ * A service that provides conditions about boolean state.
+ * <p>To extend this class, you must declare the service in your manifest file with
+ * the {@link android.Manifest.permission#BIND_CONDITION_PROVIDER_SERVICE} permission
+ * and include an intent filter with the {@link #SERVICE_INTERFACE} action. If you want users to be
+ * able to create and update conditions for this service to monitor, include the
+ * {@link #META_DATA_RULE_TYPE} and {@link #META_DATA_CONFIGURATION_ACTIVITY} tags and request the
+ * {@link android.Manifest.permission#ACCESS_NOTIFICATION_POLICY} permission. For example:</p>
+ * <pre>
+ * <service android:name=".MyConditionProvider"
+ * android:label="@string/service_name"
+ * android:permission="android.permission.BIND_CONDITION_PROVIDER_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.notification.ConditionProviderService" />
+ * </intent-filter>
+ * <meta-data
+ * android:name="android.service.zen.automatic.ruleType"
+ * android:value="@string/my_condition_rule">
+ * </meta-data>
+ * <meta-data
+ * android:name="android.service.zen.automatic.configurationActivity"
+ * android:value="com.my.package/.MyConditionConfigurationActivity">
+ * </meta-data>
+ * </service></pre>
+ *
+ * <p> Condition providers cannot be bound by the system on
+ * {@link ActivityManager#isLowRamDevice() low ram} devices running Android Q (and below)</p>
+ *
+ * @deprecated Instead of using an automatically bound service, use
+ * {@link android.app.NotificationManager#setAutomaticZenRuleState(String, Condition)} to tell the
+ * system about the state of your rule. In order to maintain a link from
+ * Settings to your rule configuration screens, provide a configuration activity that handles
+ * {@link android.app.NotificationManager#ACTION_AUTOMATIC_ZEN_RULE} on your
+ * {@link android.app.AutomaticZenRule} via
+ * {@link android.app.AutomaticZenRule#setConfigurationActivity(ComponentName)}.
+ */
+@Deprecated
+public abstract class ConditionProviderService extends Service {
+ private final String TAG = ConditionProviderService.class.getSimpleName()
+ + "[" + getClass().getSimpleName() + "]";
+
+ private final H mHandler = new H();
+
+ private Provider mProvider;
+ private INotificationManager mNoMan;
+ boolean mIsConnected;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE
+ = "android.service.notification.ConditionProviderService";
+
+ /**
+ * The name of the {@code meta-data} tag containing a localized name of the type of zen rules
+ * provided by this service.
+ *
+ * @deprecated see {@link android.app.NotificationManager#META_DATA_AUTOMATIC_RULE_TYPE}.
+ */
+ @Deprecated
+ public static final String META_DATA_RULE_TYPE = "android.service.zen.automatic.ruleType";
+
+ /**
+ * The name of the {@code meta-data} tag containing the {@link ComponentName} of an activity
+ * that allows users to configure the conditions provided by this service.
+ *
+ * @deprecated see {@link android.app.NotificationManager#ACTION_AUTOMATIC_ZEN_RULE}.
+ */
+ @Deprecated
+ public static final String META_DATA_CONFIGURATION_ACTIVITY =
+ "android.service.zen.automatic.configurationActivity";
+
+ /**
+ * The name of the {@code meta-data} tag containing the maximum number of rule instances that
+ * can be created for this rule type. Omit or enter a value <= 0 to allow unlimited instances.
+ *
+ * @deprecated see {@link android.app.NotificationManager#META_DATA_RULE_INSTANCE_LIMIT}.
+ */
+ @Deprecated
+ public static final String META_DATA_RULE_INSTANCE_LIMIT =
+ "android.service.zen.automatic.ruleInstanceLimit";
+
+ /**
+ * A String rule id extra passed to {@link #META_DATA_CONFIGURATION_ACTIVITY}.
+ *
+ * @deprecated see {@link android.app.NotificationManager#EXTRA_AUTOMATIC_RULE_ID}.
+ */
+ @Deprecated
+ public static final String EXTRA_RULE_ID = "android.service.notification.extra.RULE_ID";
+
+ /**
+ * Called when this service is connected.
+ */
+ abstract public void onConnected();
+
+ public void onRequestConditions(int relevance) {}
+
+ /**
+ * Called by the system when there is a new {@link Condition} to be managed by this provider.
+ * @param conditionId the Uri describing the criteria of the condition.
+ */
+ abstract public void onSubscribe(Uri conditionId);
+
+ /**
+ * Called by the system when a {@link Condition} has been deleted.
+ * @param conditionId the Uri describing the criteria of the deleted condition.
+ */
+ abstract public void onUnsubscribe(Uri conditionId);
+
+ private final INotificationManager getNotificationInterface() {
+ if (mNoMan == null) {
+ mNoMan = INotificationManager.Stub.asInterface(
+ ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+ }
+ return mNoMan;
+ }
+
+ /**
+ * Request that the provider be rebound, after a previous call to (@link #requestUnbind).
+ *
+ * <p>This method will fail for providers that have not been granted the permission by the user.
+ */
+ public static final void requestRebind(ComponentName componentName) {
+ INotificationManager noMan = INotificationManager.Stub.asInterface(
+ ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+ try {
+ noMan.requestBindProvider(componentName);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Request that the provider service be unbound.
+ *
+ * <p>This will no longer receive subscription updates and will not be able to update the
+ * state of conditions until {@link #requestRebind(ComponentName)} is called.
+ * The service will likely be killed by the system after this call.
+ *
+ * <p>The service should wait for the {@link #onConnected()} event before performing this
+ * operation.
+ */
+ public final void requestUnbind() {
+ INotificationManager noMan = getNotificationInterface();
+ try {
+ noMan.requestUnbindProvider(mProvider);
+ // Disable future messages.
+ mIsConnected = false;
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Informs the notification manager that the state of a Condition has changed. Use this method
+ * to put the system into Do Not Disturb mode or request that it exits Do Not Disturb mode. This
+ * call will be ignored unless there is an enabled {@link android.app.AutomaticZenRule} owned by
+ * service that has an {@link android.app.AutomaticZenRule#getConditionId()} equal to this
+ * {@link Condition#id}.
+ * @param condition the condition that has changed.
+ *
+ * @deprecated see
+ * {@link android.app.NotificationManager#setAutomaticZenRuleState(String, Condition)}.
+ */
+ @Deprecated
+ public final void notifyCondition(Condition condition) {
+ if (condition == null) return;
+ notifyConditions(new Condition[]{ condition });
+ }
+
+ /**
+ * Informs the notification manager that the state of one or more Conditions has changed. See
+ * {@link #notifyCondition(Condition)} for restrictions.
+ * @param conditions the changed conditions.
+ *
+ * @deprecated see
+ * {@link android.app.NotificationManager#setAutomaticZenRuleState(String, Condition)}.
+ */
+ @Deprecated
+ public final void notifyConditions(Condition... conditions) {
+ if (!isBound() || conditions == null) return;
+ try {
+ getNotificationInterface().notifyConditions(getPackageName(), mProvider, conditions);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (mProvider == null) {
+ mProvider = new Provider();
+ }
+ return mProvider;
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public boolean isBound() {
+ if (!mIsConnected) {
+ Log.w(TAG, "Condition provider service not yet bound.");
+ }
+ return mIsConnected;
+ }
+
+ private final class Provider extends IConditionProvider.Stub {
+ @Override
+ public void onConnected() {
+ mIsConnected = true;
+ mHandler.obtainMessage(H.ON_CONNECTED).sendToTarget();
+ }
+
+ @Override
+ public void onSubscribe(Uri conditionId) {
+ mHandler.obtainMessage(H.ON_SUBSCRIBE, conditionId).sendToTarget();
+ }
+
+ @Override
+ public void onUnsubscribe(Uri conditionId) {
+ mHandler.obtainMessage(H.ON_UNSUBSCRIBE, conditionId).sendToTarget();
+ }
+ }
+
+ private final class H extends Handler {
+ private static final int ON_CONNECTED = 1;
+ private static final int ON_SUBSCRIBE = 3;
+ private static final int ON_UNSUBSCRIBE = 4;
+
+ @Override
+ public void handleMessage(Message msg) {
+ String name = null;
+ if (!mIsConnected) {
+ return;
+ }
+ try {
+ switch(msg.what) {
+ case ON_CONNECTED:
+ name = "onConnected";
+ onConnected();
+ break;
+ case ON_SUBSCRIBE:
+ name = "onSubscribe";
+ onSubscribe((Uri)msg.obj);
+ break;
+ case ON_UNSUBSCRIBE:
+ name = "onUnsubscribe";
+ onUnsubscribe((Uri)msg.obj);
+ break;
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "Error running " + name, t);
+ }
+ }
+ }
+}
diff --git a/android/service/notification/ConversationChannelWrapper.java b/android/service/notification/ConversationChannelWrapper.java
new file mode 100644
index 0000000..ab465ab
--- /dev/null
+++ b/android/service/notification/ConversationChannelWrapper.java
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2020, 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.notification;
+
+import android.app.NotificationChannel;
+import android.content.pm.ShortcutInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+public final class ConversationChannelWrapper implements Parcelable {
+
+ private NotificationChannel mNotificationChannel;
+ private CharSequence mGroupLabel;
+ private CharSequence mParentChannelLabel;
+ private ShortcutInfo mShortcutInfo;
+ private String mPkg;
+ private int mUid;
+
+ public ConversationChannelWrapper() {}
+
+ protected ConversationChannelWrapper(Parcel in) {
+ mNotificationChannel = in.readParcelable(NotificationChannel.class.getClassLoader());
+ mGroupLabel = in.readCharSequence();
+ mParentChannelLabel = in.readCharSequence();
+ mShortcutInfo = in.readParcelable(ShortcutInfo.class.getClassLoader());
+ mPkg = in.readStringNoHelper();
+ mUid = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mNotificationChannel, flags);
+ dest.writeCharSequence(mGroupLabel);
+ dest.writeCharSequence(mParentChannelLabel);
+ dest.writeParcelable(mShortcutInfo, flags);
+ dest.writeStringNoHelper(mPkg);
+ dest.writeInt(mUid);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<ConversationChannelWrapper> CREATOR =
+ new Creator<ConversationChannelWrapper>() {
+ @Override
+ public ConversationChannelWrapper createFromParcel(Parcel in) {
+ return new ConversationChannelWrapper(in);
+ }
+
+ @Override
+ public ConversationChannelWrapper[] newArray(int size) {
+ return new ConversationChannelWrapper[size];
+ }
+ };
+
+
+ public NotificationChannel getNotificationChannel() {
+ return mNotificationChannel;
+ }
+
+ public void setNotificationChannel(
+ NotificationChannel notificationChannel) {
+ mNotificationChannel = notificationChannel;
+ }
+
+ public CharSequence getGroupLabel() {
+ return mGroupLabel;
+ }
+
+ public void setGroupLabel(CharSequence groupLabel) {
+ mGroupLabel = groupLabel;
+ }
+
+ public CharSequence getParentChannelLabel() {
+ return mParentChannelLabel;
+ }
+
+ public void setParentChannelLabel(CharSequence parentChannelLabel) {
+ mParentChannelLabel = parentChannelLabel;
+ }
+
+ public ShortcutInfo getShortcutInfo() {
+ return mShortcutInfo;
+ }
+
+ public void setShortcutInfo(ShortcutInfo shortcutInfo) {
+ mShortcutInfo = shortcutInfo;
+ }
+
+ public String getPkg() {
+ return mPkg;
+ }
+
+ public void setPkg(String pkg) {
+ mPkg = pkg;
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public void setUid(int uid) {
+ mUid = uid;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ConversationChannelWrapper that = (ConversationChannelWrapper) o;
+ return Objects.equals(getNotificationChannel(), that.getNotificationChannel()) &&
+ Objects.equals(getGroupLabel(), that.getGroupLabel()) &&
+ Objects.equals(getParentChannelLabel(), that.getParentChannelLabel()) &&
+ Objects.equals(getShortcutInfo(), that.getShortcutInfo()) &&
+ Objects.equals(getPkg(), that.getPkg()) &&
+ getUid() == that.getUid();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getNotificationChannel(), getGroupLabel(), getParentChannelLabel(),
+ getShortcutInfo(), getPkg(), getUid());
+ }
+}
diff --git a/android/service/notification/NotificationAssistantService.java b/android/service/notification/NotificationAssistantService.java
new file mode 100644
index 0000000..975e75c
--- /dev/null
+++ b/android/service/notification/NotificationAssistantService.java
@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2015 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.notification;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.os.SomeArgs;
+
+import java.lang.annotation.Retention;
+import java.util.List;
+
+/**
+ * A service that helps the user manage notifications.
+ * <p>
+ * Only one notification assistant can be active at a time. Unlike notification listener services,
+ * assistant services can additionally modify certain aspects about notifications
+ * (see {@link Adjustment}) before they are posted.
+ *<p>
+ * A note about managed profiles: Unlike {@link NotificationListenerService listener services},
+ * NotificationAssistantServices are allowed to run in managed profiles
+ * (see {@link DevicePolicyManager#isManagedProfile(ComponentName)}), so they can access the
+ * information they need to create good {@link Adjustment adjustments}. To maintain the contract
+ * with {@link NotificationListenerService}, an assistant service will receive all of the
+ * callbacks from {@link NotificationListenerService} for the current user, managed profiles of
+ * that user, and ones that affect all users. However,
+ * {@link #onNotificationEnqueued(StatusBarNotification)} will only be called for notifications
+ * sent to the current user, and {@link Adjustment adjuments} will only be accepted for the
+ * current user.
+ * <p>
+ * All callbacks are called on the main thread.
+ * </p>
+ * @hide
+ */
+@SystemApi
+@TestApi
+public abstract class NotificationAssistantService extends NotificationListenerService {
+ private static final String TAG = "NotificationAssistants";
+
+ /** @hide */
+ @Retention(SOURCE)
+ @IntDef({SOURCE_FROM_APP, SOURCE_FROM_ASSISTANT})
+ public @interface Source {}
+
+ /**
+ * To indicate an adjustment is from an app.
+ */
+ public static final int SOURCE_FROM_APP = 0;
+ /**
+ * To indicate an adjustment is from a {@link NotificationAssistantService}.
+ */
+ public static final int SOURCE_FROM_ASSISTANT = 1;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE
+ = "android.service.notification.NotificationAssistantService";
+
+ /**
+ * @hide
+ */
+ protected Handler mHandler;
+
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ mHandler = new MyHandler(getContext().getMainLooper());
+ }
+
+ @Override
+ public final @NonNull IBinder onBind(@Nullable Intent intent) {
+ if (mWrapper == null) {
+ mWrapper = new NotificationAssistantServiceWrapper();
+ }
+ return mWrapper;
+ }
+
+ /**
+ * A notification was snoozed until a context. For use with
+ * {@link Adjustment#KEY_SNOOZE_CRITERIA}. When the device reaches the given context, the
+ * assistant should restore the notification with {@link #unsnoozeNotification(String)}.
+ *
+ * @param sbn the notification to snooze
+ * @param snoozeCriterionId the {@link SnoozeCriterion#getId()} representing a device context.
+ */
+ abstract public void onNotificationSnoozedUntilContext(@NonNull StatusBarNotification sbn,
+ @NonNull String snoozeCriterionId);
+
+ /**
+ * A notification was posted by an app. Called before post.
+ *
+ * <p>Note: this method is only called if you don't override
+ * {@link #onNotificationEnqueued(StatusBarNotification, NotificationChannel)}.</p>
+ *
+ * @param sbn the new notification
+ * @return an adjustment or null to take no action, within 100ms.
+ */
+ abstract public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification sbn);
+
+ /**
+ * A notification was posted by an app. Called before post.
+ *
+ * @param sbn the new notification
+ * @param channel the channel the notification was posted to
+ * @return an adjustment or null to take no action, within 100ms.
+ */
+ public @Nullable Adjustment onNotificationEnqueued(@NonNull StatusBarNotification sbn,
+ @NonNull NotificationChannel channel) {
+ return onNotificationEnqueued(sbn);
+ }
+
+ /**
+ * Implement this method to learn when notifications are removed, how they were interacted with
+ * before removal, and why they were removed.
+ * <p>
+ * This might occur because the user has dismissed the notification using system UI (or another
+ * notification listener) or because the app has withdrawn the notification.
+ * <p>
+ * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
+ * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
+ * fields such as {@link android.app.Notification#contentView} and
+ * {@link android.app.Notification#largeIcon}. However, all other fields on
+ * {@link StatusBarNotification}, sufficient to match this call with a prior call to
+ * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
+ *
+ ** @param sbn A data structure encapsulating at least the original information (tag and id)
+ * and source (package name) used to post the {@link android.app.Notification} that
+ * was just removed.
+ * @param rankingMap The current ranking map that can be used to retrieve ranking information
+ * for active notifications.
+ * @param stats Stats about how the user interacted with the notification before it was removed.
+ * @param reason see {@link #REASON_LISTENER_CANCEL}, etc.
+ */
+ @Override
+ public void onNotificationRemoved(@NonNull StatusBarNotification sbn,
+ @NonNull RankingMap rankingMap,
+ @NonNull NotificationStats stats, int reason) {
+ onNotificationRemoved(sbn, rankingMap, reason);
+ }
+
+ /**
+ * Implement this to know when a user has seen notifications, as triggered by
+ * {@link #setNotificationsShown(String[])}.
+ */
+ public void onNotificationsSeen(@NonNull List<String> keys) {
+
+ }
+
+ /**
+ * Implement this to know when the notification panel is revealed
+ *
+ * @param items Number of notifications on the panel at time of opening
+ */
+ public void onPanelRevealed(int items) {
+
+ }
+
+ /**
+ * Implement this to know when the notification panel is hidden
+ */
+ public void onPanelHidden() {
+
+ }
+
+ /**
+ * Implement this to know when a notification becomes visible or hidden from the user.
+ *
+ * @param key the notification key
+ * @param isVisible whether the notification is visible.
+ */
+ public void onNotificationVisibilityChanged(@NonNull String key, boolean isVisible) {
+
+ }
+
+ /**
+ * Implement this to know when a notification change (expanded / collapsed) is visible to user.
+ *
+ * @param key the notification key
+ * @param isUserAction whether the expanded change is caused by user action.
+ * @param isExpanded whether the notification is expanded.
+ */
+ public void onNotificationExpansionChanged(
+ @NonNull String key, boolean isUserAction, boolean isExpanded) {}
+
+ /**
+ * Implement this to know when a direct reply is sent from a notification.
+ * @param key the notification key
+ */
+ public void onNotificationDirectReplied(@NonNull String key) {}
+
+ /**
+ * Implement this to know when a suggested reply is sent.
+ * @param key the notification key
+ * @param reply the reply that is just sent
+ * @param source the source that provided the reply, e.g. SOURCE_FROM_APP
+ */
+ public void onSuggestedReplySent(@NonNull String key, @NonNull CharSequence reply,
+ @Source int source) {
+ }
+
+ /**
+ * Implement this to know when an action is clicked.
+ * @param key the notification key
+ * @param action the action that is just clicked
+ * @param source the source that provided the action, e.g. SOURCE_FROM_APP
+ */
+ public void onActionInvoked(@NonNull String key, @NonNull Notification.Action action,
+ @Source int source) {
+ }
+
+ /**
+ * Implement this to know when a user has changed which features of
+ * their notifications the assistant can modify.
+ * <p> Query {@link NotificationManager#getAllowedAssistantAdjustments()} to see what
+ * {@link Adjustment adjustments} you are currently allowed to make.</p>
+ */
+ public void onAllowedAdjustmentsChanged() {
+ }
+
+ /**
+ * Updates a notification. N.B. this won’t cause
+ * an existing notification to alert, but might allow a future update to
+ * this notification to alert.
+ *
+ * @param adjustment the adjustment with an explanation
+ */
+ public final void adjustNotification(@NonNull Adjustment adjustment) {
+ if (!isBound()) return;
+ try {
+ setAdjustmentIssuer(adjustment);
+ getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(mWrapper, adjustment);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Updates existing notifications. Re-ranking won't occur until all adjustments are applied.
+ * N.B. this won’t cause an existing notification to alert, but might allow a future update to
+ * these notifications to alert.
+ *
+ * @param adjustments a list of adjustments with explanations
+ */
+ public final void adjustNotifications(@NonNull List<Adjustment> adjustments) {
+ if (!isBound()) return;
+ try {
+ for (Adjustment adjustment : adjustments) {
+ setAdjustmentIssuer(adjustment);
+ }
+ getNotificationInterface().applyAdjustmentsFromAssistant(mWrapper, adjustments);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Inform the notification manager about un-snoozing a specific notification.
+ * <p>
+ * This should only be used for notifications snoozed because of a contextual snooze suggestion
+ * you provided via {@link Adjustment#KEY_SNOOZE_CRITERIA}. Once un-snoozed, you will get a
+ * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the
+ * notification.
+ * @param key The key of the notification to snooze
+ */
+ public final void unsnoozeNotification(@NonNull String key) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().unsnoozeNotificationFromAssistant(mWrapper, key);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+ private class NotificationAssistantServiceWrapper extends NotificationListenerWrapper {
+ @Override
+ public void onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder,
+ NotificationChannel channel) {
+ StatusBarNotification sbn;
+ try {
+ sbn = sbnHolder.get();
+ } catch (RemoteException e) {
+ Log.w(TAG, "onNotificationEnqueued: Error receiving StatusBarNotification", e);
+ return;
+ }
+ if (sbn == null) {
+ Log.w(TAG, "onNotificationEnqueuedWithChannel: "
+ + "Error receiving StatusBarNotification");
+ return;
+ }
+
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = sbn;
+ args.arg2 = channel;
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_ENQUEUED,
+ args).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationSnoozedUntilContext(
+ IStatusBarNotificationHolder sbnHolder, String snoozeCriterionId) {
+ StatusBarNotification sbn;
+ try {
+ sbn = sbnHolder.get();
+ } catch (RemoteException e) {
+ Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification", e);
+ return;
+ }
+ if (sbn == null) {
+ Log.w(TAG, "onNotificationSnoozed: Error receiving StatusBarNotification");
+ return;
+ }
+
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = sbn;
+ args.arg2 = snoozeCriterionId;
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_SNOOZED,
+ args).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationsSeen(List<String> keys) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = keys;
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATIONS_SEEN,
+ args).sendToTarget();
+ }
+
+ @Override
+ public void onPanelRevealed(int items) {
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = items;
+ mHandler.obtainMessage(MyHandler.MSG_ON_PANEL_REVEALED,
+ args).sendToTarget();
+ }
+
+ @Override
+ public void onPanelHidden() {
+ SomeArgs args = SomeArgs.obtain();
+ mHandler.obtainMessage(MyHandler.MSG_ON_PANEL_HIDDEN,
+ args).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationVisibilityChanged(String key, boolean isVisible) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = key;
+ args.argi1 = isVisible ? 1 : 0;
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_VISIBILITY_CHANGED,
+ args).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationExpansionChanged(String key, boolean isUserAction,
+ boolean isExpanded) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = key;
+ args.argi1 = isUserAction ? 1 : 0;
+ args.argi2 = isExpanded ? 1 : 0;
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_EXPANSION_CHANGED, args)
+ .sendToTarget();
+ }
+
+ @Override
+ public void onNotificationDirectReply(String key) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = key;
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT, args)
+ .sendToTarget();
+ }
+
+ @Override
+ public void onSuggestedReplySent(String key, CharSequence reply, int source) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = key;
+ args.arg2 = reply;
+ args.argi2 = source;
+ mHandler.obtainMessage(MyHandler.MSG_ON_SUGGESTED_REPLY_SENT, args).sendToTarget();
+ }
+
+ @Override
+ public void onActionClicked(String key, Notification.Action action, int source) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = key;
+ args.arg2 = action;
+ args.argi2 = source;
+ mHandler.obtainMessage(MyHandler.MSG_ON_ACTION_INVOKED, args).sendToTarget();
+ }
+
+ @Override
+ public void onAllowedAdjustmentsChanged() {
+ mHandler.obtainMessage(MyHandler.MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED).sendToTarget();
+ }
+ }
+
+ private void setAdjustmentIssuer(@Nullable Adjustment adjustment) {
+ if (adjustment != null) {
+ adjustment.setIssuer(getOpPackageName() + "/" + getClass().getName());
+ }
+ }
+
+ private final class MyHandler extends Handler {
+ public static final int MSG_ON_NOTIFICATION_ENQUEUED = 1;
+ public static final int MSG_ON_NOTIFICATION_SNOOZED = 2;
+ public static final int MSG_ON_NOTIFICATIONS_SEEN = 3;
+ public static final int MSG_ON_NOTIFICATION_EXPANSION_CHANGED = 4;
+ public static final int MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT = 5;
+ public static final int MSG_ON_SUGGESTED_REPLY_SENT = 6;
+ public static final int MSG_ON_ACTION_INVOKED = 7;
+ public static final int MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED = 8;
+ public static final int MSG_ON_PANEL_REVEALED = 9;
+ public static final int MSG_ON_PANEL_HIDDEN = 10;
+ public static final int MSG_ON_NOTIFICATION_VISIBILITY_CHANGED = 11;
+
+ public MyHandler(Looper looper) {
+ super(looper, null, false);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ON_NOTIFICATION_ENQUEUED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ StatusBarNotification sbn = (StatusBarNotification) args.arg1;
+ NotificationChannel channel = (NotificationChannel) args.arg2;
+ args.recycle();
+ Adjustment adjustment = onNotificationEnqueued(sbn, channel);
+ setAdjustmentIssuer(adjustment);
+ if (adjustment != null) {
+ if (!isBound()) {
+ Log.w(TAG, "MSG_ON_NOTIFICATION_ENQUEUED: service not bound, skip.");
+ return;
+ }
+ try {
+ getNotificationInterface().applyEnqueuedAdjustmentFromAssistant(
+ mWrapper, adjustment);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ throw ex.rethrowFromSystemServer();
+ } catch (SecurityException e) {
+ // app cannot catch and recover from this, so do on their behalf
+ Log.w(TAG, "Enqueue adjustment failed; no longer connected", e);
+ }
+ }
+ break;
+ }
+ case MSG_ON_NOTIFICATION_SNOOZED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ StatusBarNotification sbn = (StatusBarNotification) args.arg1;
+ String snoozeCriterionId = (String) args.arg2;
+ args.recycle();
+ onNotificationSnoozedUntilContext(sbn, snoozeCriterionId);
+ break;
+ }
+ case MSG_ON_NOTIFICATIONS_SEEN: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ List<String> keys = (List<String>) args.arg1;
+ args.recycle();
+ onNotificationsSeen(keys);
+ break;
+ }
+ case MSG_ON_NOTIFICATION_EXPANSION_CHANGED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ String key = (String) args.arg1;
+ boolean isUserAction = args.argi1 == 1;
+ boolean isExpanded = args.argi2 == 1;
+ args.recycle();
+ onNotificationExpansionChanged(key, isUserAction, isExpanded);
+ break;
+ }
+ case MSG_ON_NOTIFICATION_DIRECT_REPLY_SENT: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ String key = (String) args.arg1;
+ args.recycle();
+ onNotificationDirectReplied(key);
+ break;
+ }
+ case MSG_ON_SUGGESTED_REPLY_SENT: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ String key = (String) args.arg1;
+ CharSequence reply = (CharSequence) args.arg2;
+ int source = args.argi2;
+ args.recycle();
+ onSuggestedReplySent(key, reply, source);
+ break;
+ }
+ case MSG_ON_ACTION_INVOKED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ String key = (String) args.arg1;
+ Notification.Action action = (Notification.Action) args.arg2;
+ int source = args.argi2;
+ args.recycle();
+ onActionInvoked(key, action, source);
+ break;
+ }
+ case MSG_ON_ALLOWED_ADJUSTMENTS_CHANGED: {
+ onAllowedAdjustmentsChanged();
+ break;
+ }
+ case MSG_ON_PANEL_REVEALED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ int items = args.argi1;
+ args.recycle();
+ onPanelRevealed(items);
+ break;
+ }
+ case MSG_ON_PANEL_HIDDEN: {
+ onPanelHidden();
+ break;
+ }
+ case MSG_ON_NOTIFICATION_VISIBILITY_CHANGED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ String key = (String) args.arg1;
+ boolean isVisible = args.argi1 == 1;
+ args.recycle();
+ onNotificationVisibilityChanged(key, isVisible);
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java
new file mode 100644
index 0000000..c52b02b
--- /dev/null
+++ b/android/service/notification/NotificationListenerService.java
@@ -0,0 +1,2187 @@
+/*
+ * Copyright (C) 2013 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.notification;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.ActivityManager;
+import android.app.INotificationManager;
+import android.app.Notification;
+import android.app.Notification.Builder;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
+import android.app.Person;
+import android.app.Service;
+import android.companion.CompanionDeviceManager;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A service that receives calls from the system when new notifications are
+ * posted or removed, or their ranking changed.
+ * <p>To extend this class, you must declare the service in your manifest file with
+ * the {@link android.Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE} permission
+ * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
+ * <pre>
+ * <service android:name=".NotificationListener"
+ * android:label="@string/service_name"
+ * android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.notification.NotificationListenerService" />
+ * </intent-filter>
+ * </service></pre>
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing any operations. The {@link #requestRebind(ComponentName)}
+ * method is the <i>only</i> one that is safe to call before {@link #onListenerConnected()}
+ * or after {@link #onListenerDisconnected()}.
+ * </p>
+ * <p> Notification listeners cannot get notification access or be bound by the system on
+ * {@linkplain ActivityManager#isLowRamDevice() low-RAM} devices running Android Q (and below).
+ * The system also ignores notification listeners running in a work profile. A
+ * {@link android.app.admin.DevicePolicyManager} might block notifications originating from a work
+ * profile.</p>
+ * <p>
+ * From {@link Build.VERSION_CODES#N} onward all callbacks are called on the main thread. Prior
+ * to N, there is no guarantee on what thread the callback will happen.
+ * </p>
+ */
+public abstract class NotificationListenerService extends Service {
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private final String TAG = getClass().getSimpleName();
+
+ /**
+ * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
+ * Normal interruption filter.
+ */
+ public static final int INTERRUPTION_FILTER_ALL
+ = NotificationManager.INTERRUPTION_FILTER_ALL;
+
+ /**
+ * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
+ * Priority interruption filter.
+ */
+ public static final int INTERRUPTION_FILTER_PRIORITY
+ = NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+ /**
+ * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
+ * No interruptions filter.
+ */
+ public static final int INTERRUPTION_FILTER_NONE
+ = NotificationManager.INTERRUPTION_FILTER_NONE;
+
+ /**
+ * {@link #getCurrentInterruptionFilter() Interruption filter} constant -
+ * Alarms only interruption filter.
+ */
+ public static final int INTERRUPTION_FILTER_ALARMS
+ = NotificationManager.INTERRUPTION_FILTER_ALARMS;
+
+ /** {@link #getCurrentInterruptionFilter() Interruption filter} constant - returned when
+ * the value is unavailable for any reason. For example, before the notification listener
+ * is connected.
+ *
+ * {@see #onListenerConnected()}
+ */
+ public static final int INTERRUPTION_FILTER_UNKNOWN
+ = NotificationManager.INTERRUPTION_FILTER_UNKNOWN;
+
+ /** {@link #getCurrentListenerHints() Listener hints} constant - the primary device UI
+ * should disable notification sound, vibrating and other visual or aural effects.
+ * This does not change the interruption filter, only the effects. **/
+ public static final int HINT_HOST_DISABLE_EFFECTS = 1;
+
+ /** {@link #getCurrentListenerHints() Listener hints} constant - the primary device UI
+ * should disable notification sound, but not phone calls.
+ * This does not change the interruption filter, only the effects. **/
+ public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 1 << 1;
+
+ /** {@link #getCurrentListenerHints() Listener hints} constant - the primary device UI
+ * should disable phone call sounds, buyt not notification sound.
+ * This does not change the interruption filter, only the effects. **/
+ public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 1 << 2;
+
+ /**
+ * Whether notification suppressed by DND should not interruption visually when the screen is
+ * off.
+ *
+ * @deprecated Use the more specific visual effects in {@link NotificationManager.Policy}.
+ */
+ @Deprecated
+ public static final int SUPPRESSED_EFFECT_SCREEN_OFF =
+ NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
+ /**
+ * Whether notification suppressed by DND should not interruption visually when the screen is
+ * on.
+ *
+ * @deprecated Use the more specific visual effects in {@link NotificationManager.Policy}.
+ */
+ @Deprecated
+ public static final int SUPPRESSED_EFFECT_SCREEN_ON =
+ NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON;
+
+
+ // Notification cancellation reasons
+
+ /** Notification was canceled by the status bar reporting a notification click. */
+ public static final int REASON_CLICK = 1;
+ /** Notification was canceled by the status bar reporting a user dismissal. */
+ public static final int REASON_CANCEL = 2;
+ /** Notification was canceled by the status bar reporting a user dismiss all. */
+ public static final int REASON_CANCEL_ALL = 3;
+ /** Notification was canceled by the status bar reporting an inflation error. */
+ public static final int REASON_ERROR = 4;
+ /** Notification was canceled by the package manager modifying the package. */
+ public static final int REASON_PACKAGE_CHANGED = 5;
+ /** Notification was canceled by the owning user context being stopped. */
+ public static final int REASON_USER_STOPPED = 6;
+ /** Notification was canceled by the user banning the package. */
+ public static final int REASON_PACKAGE_BANNED = 7;
+ /** Notification was canceled by the app canceling this specific notification. */
+ public static final int REASON_APP_CANCEL = 8;
+ /** Notification was canceled by the app cancelling all its notifications. */
+ public static final int REASON_APP_CANCEL_ALL = 9;
+ /** Notification was canceled by a listener reporting a user dismissal. */
+ public static final int REASON_LISTENER_CANCEL = 10;
+ /** Notification was canceled by a listener reporting a user dismiss all. */
+ public static final int REASON_LISTENER_CANCEL_ALL = 11;
+ /** Notification was canceled because it was a member of a canceled group. */
+ public static final int REASON_GROUP_SUMMARY_CANCELED = 12;
+ /** Notification was canceled because it was an invisible member of a group. */
+ public static final int REASON_GROUP_OPTIMIZATION = 13;
+ /** Notification was canceled by the device administrator suspending the package. */
+ public static final int REASON_PACKAGE_SUSPENDED = 14;
+ /** Notification was canceled by the owning managed profile being turned off. */
+ public static final int REASON_PROFILE_TURNED_OFF = 15;
+ /** Autobundled summary notification was canceled because its group was unbundled */
+ public static final int REASON_UNAUTOBUNDLED = 16;
+ /** Notification was canceled by the user banning the channel. */
+ public static final int REASON_CHANNEL_BANNED = 17;
+ /** Notification was snoozed. */
+ public static final int REASON_SNOOZED = 18;
+ /** Notification was canceled due to timeout */
+ public static final int REASON_TIMEOUT = 19;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = "REASON_", value = {
+ REASON_CLICK,
+ REASON_CANCEL,
+ REASON_CANCEL_ALL,
+ REASON_ERROR,
+ REASON_PACKAGE_CHANGED,
+ REASON_USER_STOPPED,
+ REASON_PACKAGE_BANNED,
+ REASON_APP_CANCEL,
+ REASON_APP_CANCEL_ALL,
+ REASON_LISTENER_CANCEL,
+ REASON_LISTENER_CANCEL_ALL,
+ REASON_GROUP_SUMMARY_CANCELED,
+ REASON_GROUP_OPTIMIZATION,
+ REASON_PACKAGE_SUSPENDED,
+ REASON_PROFILE_TURNED_OFF,
+ REASON_UNAUTOBUNDLED,
+ REASON_CHANNEL_BANNED,
+ REASON_SNOOZED,
+ REASON_TIMEOUT
+ })
+ public @interface NotificationCancelReason{};
+
+ /**
+ * The full trim of the StatusBarNotification including all its features.
+ *
+ * @hide
+ * @removed
+ */
+ @SystemApi
+ public static final int TRIM_FULL = 0;
+
+ /**
+ * A light trim of the StatusBarNotification excluding the following features:
+ *
+ * <ol>
+ * <li>{@link Notification#tickerView tickerView}</li>
+ * <li>{@link Notification#contentView contentView}</li>
+ * <li>{@link Notification#largeIcon largeIcon}</li>
+ * <li>{@link Notification#bigContentView bigContentView}</li>
+ * <li>{@link Notification#headsUpContentView headsUpContentView}</li>
+ * <li>{@link Notification#EXTRA_LARGE_ICON extras[EXTRA_LARGE_ICON]}</li>
+ * <li>{@link Notification#EXTRA_LARGE_ICON_BIG extras[EXTRA_LARGE_ICON_BIG]}</li>
+ * <li>{@link Notification#EXTRA_PICTURE extras[EXTRA_PICTURE]}</li>
+ * <li>{@link Notification#EXTRA_BIG_TEXT extras[EXTRA_BIG_TEXT]}</li>
+ * </ol>
+ *
+ * @hide
+ * @removed
+ */
+ @SystemApi
+ public static final int TRIM_LIGHT = 1;
+
+
+ /** @hide */
+ @IntDef(prefix = { "NOTIFICATION_CHANNEL_OR_GROUP_" }, value = {
+ NOTIFICATION_CHANNEL_OR_GROUP_ADDED,
+ NOTIFICATION_CHANNEL_OR_GROUP_UPDATED,
+ NOTIFICATION_CHANNEL_OR_GROUP_DELETED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ChannelOrGroupModificationTypes {}
+
+ /**
+ * Channel or group modification reason provided to
+ * {@link #onNotificationChannelModified(String, UserHandle,NotificationChannel, int)} or
+ * {@link #onNotificationChannelGroupModified(String, UserHandle, NotificationChannelGroup,
+ * int)}- the provided object was created.
+ */
+ public static final int NOTIFICATION_CHANNEL_OR_GROUP_ADDED = 1;
+
+ /**
+ * Channel or group modification reason provided to
+ * {@link #onNotificationChannelModified(String, UserHandle, NotificationChannel, int)} or
+ * {@link #onNotificationChannelGroupModified(String, UserHandle,NotificationChannelGroup, int)}
+ * - the provided object was updated.
+ */
+ public static final int NOTIFICATION_CHANNEL_OR_GROUP_UPDATED = 2;
+
+ /**
+ * Channel or group modification reason provided to
+ * {@link #onNotificationChannelModified(String, UserHandle, NotificationChannel, int)} or
+ * {@link #onNotificationChannelGroupModified(String, UserHandle, NotificationChannelGroup,
+ * int)}- the provided object was deleted.
+ */
+ public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3;
+
+ private final Object mLock = new Object();
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private Handler mHandler;
+
+ /** @hide */
+ @UnsupportedAppUsage
+ protected NotificationListenerWrapper mWrapper = null;
+ private boolean isConnected = false;
+
+ @GuardedBy("mLock")
+ private RankingMap mRankingMap;
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ protected INotificationManager mNoMan;
+
+ /**
+ * Only valid after a successful call to (@link registerAsService}.
+ * @hide
+ */
+ protected int mCurrentUser;
+
+ /**
+ * This context is required for system services since NotificationListenerService isn't
+ * started as a real Service and hence no context is available..
+ * @hide
+ */
+ protected Context mSystemContext;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE
+ = "android.service.notification.NotificationListenerService";
+
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ mHandler = new MyHandler(getMainLooper());
+ }
+
+ /**
+ * Implement this method to learn about new notifications as they are posted by apps.
+ *
+ * @param sbn A data structure encapsulating the original {@link android.app.Notification}
+ * object as well as its identifying information (tag and id) and source
+ * (package name).
+ */
+ public void onNotificationPosted(StatusBarNotification sbn) {
+ // optional
+ }
+
+ /**
+ * Implement this method to learn about new notifications as they are posted by apps.
+ *
+ * @param sbn A data structure encapsulating the original {@link android.app.Notification}
+ * object as well as its identifying information (tag and id) and source
+ * (package name).
+ * @param rankingMap The current ranking map that can be used to retrieve ranking information
+ * for active notifications, including the newly posted one.
+ */
+ public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
+ onNotificationPosted(sbn);
+ }
+
+ /**
+ * Implement this method to learn when notifications are removed.
+ * <p>
+ * This might occur because the user has dismissed the notification using system UI (or another
+ * notification listener) or because the app has withdrawn the notification.
+ * <p>
+ * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
+ * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
+ * fields such as {@link android.app.Notification#contentView} and
+ * {@link android.app.Notification#largeIcon}. However, all other fields on
+ * {@link StatusBarNotification}, sufficient to match this call with a prior call to
+ * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
+ *
+ * @param sbn A data structure encapsulating at least the original information (tag and id)
+ * and source (package name) used to post the {@link android.app.Notification} that
+ * was just removed.
+ */
+ public void onNotificationRemoved(StatusBarNotification sbn) {
+ // optional
+ }
+
+ /**
+ * Implement this method to learn when notifications are removed.
+ * <p>
+ * This might occur because the user has dismissed the notification using system UI (or another
+ * notification listener) or because the app has withdrawn the notification.
+ * <p>
+ * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
+ * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
+ * fields such as {@link android.app.Notification#contentView} and
+ * {@link android.app.Notification#largeIcon}. However, all other fields on
+ * {@link StatusBarNotification}, sufficient to match this call with a prior call to
+ * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
+ *
+ * @param sbn A data structure encapsulating at least the original information (tag and id)
+ * and source (package name) used to post the {@link android.app.Notification} that
+ * was just removed.
+ * @param rankingMap The current ranking map that can be used to retrieve ranking information
+ * for active notifications.
+ *
+ */
+ public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
+ onNotificationRemoved(sbn);
+ }
+
+
+ /**
+ * Implement this method to learn when notifications are removed and why.
+ * <p>
+ * This might occur because the user has dismissed the notification using system UI (or another
+ * notification listener) or because the app has withdrawn the notification.
+ * <p>
+ * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
+ * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
+ * fields such as {@link android.app.Notification#contentView} and
+ * {@link android.app.Notification#largeIcon}. However, all other fields on
+ * {@link StatusBarNotification}, sufficient to match this call with a prior call to
+ * {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
+ *
+ ** @param sbn A data structure encapsulating at least the original information (tag and id)
+ * and source (package name) used to post the {@link android.app.Notification} that
+ * was just removed.
+ * @param rankingMap The current ranking map that can be used to retrieve ranking information
+ * for active notifications.
+ * @param reason see {@link #REASON_LISTENER_CANCEL}, etc.
+ */
+ public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
+ int reason) {
+ onNotificationRemoved(sbn, rankingMap);
+ }
+
+ /**
+ * NotificationStats are not populated for notification listeners, so fall back to
+ * {@link #onNotificationRemoved(StatusBarNotification, RankingMap, int)}.
+ *
+ * @hide
+ */
+ @TestApi
+ @SystemApi
+ public void onNotificationRemoved(@NonNull StatusBarNotification sbn,
+ @NonNull RankingMap rankingMap, @NonNull NotificationStats stats, int reason) {
+ onNotificationRemoved(sbn, rankingMap, reason);
+ }
+
+ /**
+ * Implement this method to learn about when the listener is enabled and connected to
+ * the notification manager. You are safe to call {@link #getActiveNotifications()}
+ * at this time.
+ */
+ public void onListenerConnected() {
+ // optional
+ }
+
+ /**
+ * Implement this method to learn about when the listener is disconnected from the
+ * notification manager.You will not receive any events after this call, and may only
+ * call {@link #requestRebind(ComponentName)} at this time.
+ */
+ public void onListenerDisconnected() {
+ // optional
+ }
+
+ /**
+ * Implement this method to be notified when the notification ranking changes.
+ *
+ * @param rankingMap The current ranking map that can be used to retrieve ranking information
+ * for active notifications.
+ */
+ public void onNotificationRankingUpdate(RankingMap rankingMap) {
+ // optional
+ }
+
+ /**
+ * Implement this method to be notified when the
+ * {@link #getCurrentListenerHints() Listener hints} change.
+ *
+ * @param hints The current {@link #getCurrentListenerHints() listener hints}.
+ */
+ public void onListenerHintsChanged(int hints) {
+ // optional
+ }
+
+ /**
+ * Implement this method to be notified when the behavior of silent notifications in the status
+ * bar changes. See {@link NotificationManager#shouldHideSilentStatusBarIcons()}.
+ *
+ * @param hideSilentStatusIcons whether or not status bar icons should be hidden for silent
+ * notifications
+ */
+ public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) {
+ // optional
+ }
+
+ /**
+ * Implement this method to learn about notification channel modifications.
+ *
+ * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+ * device} in order to receive this callback.
+ *
+ * @param pkg The package the channel belongs to.
+ * @param user The user on which the change was made.
+ * @param channel The channel that has changed.
+ * @param modificationType One of {@link #NOTIFICATION_CHANNEL_OR_GROUP_ADDED},
+ * {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED},
+ * {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}.
+ */
+ public void onNotificationChannelModified(String pkg, UserHandle user,
+ NotificationChannel channel, @ChannelOrGroupModificationTypes int modificationType) {
+ // optional
+ }
+
+ /**
+ * Implement this method to learn about notification channel group modifications.
+ *
+ * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+ * device} in order to receive this callback.
+ *
+ * @param pkg The package the group belongs to.
+ * @param user The user on which the change was made.
+ * @param group The group that has changed.
+ * @param modificationType One of {@link #NOTIFICATION_CHANNEL_OR_GROUP_ADDED},
+ * {@link #NOTIFICATION_CHANNEL_OR_GROUP_UPDATED},
+ * {@link #NOTIFICATION_CHANNEL_OR_GROUP_DELETED}.
+ */
+ public void onNotificationChannelGroupModified(String pkg, UserHandle user,
+ NotificationChannelGroup group, @ChannelOrGroupModificationTypes int modificationType) {
+ // optional
+ }
+
+ /**
+ * Implement this method to be notified when the
+ * {@link #getCurrentInterruptionFilter() interruption filter} changed.
+ *
+ * @param interruptionFilter The current
+ * {@link #getCurrentInterruptionFilter() interruption filter}.
+ */
+ public void onInterruptionFilterChanged(int interruptionFilter) {
+ // optional
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ protected final INotificationManager getNotificationInterface() {
+ if (mNoMan == null) {
+ mNoMan = INotificationManager.Stub.asInterface(
+ ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+ }
+ return mNoMan;
+ }
+
+ /**
+ * Inform the notification manager about dismissal of a single notification.
+ * <p>
+ * Use this if your listener has a user interface that allows the user to dismiss individual
+ * notifications, similar to the behavior of Android's status bar and notification panel.
+ * It should be called after the user dismisses a single notification using your UI;
+ * upon being informed, the notification manager will actually remove the notification
+ * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback.
+ * <p>
+ * <b>Note:</b> If your listener allows the user to fire a notification's
+ * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call
+ * this method at that time <i>if</i> the Notification in question has the
+ * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @param pkg Package of the notifying app.
+ * @param tag Tag of the notification as specified by the notifying app in
+ * {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}.
+ * @param id ID of the notification as specified by the notifying app in
+ * {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}.
+ * <p>
+ * @deprecated Use {@link #cancelNotification(String key)}
+ * instead. Beginning with {@link android.os.Build.VERSION_CODES#LOLLIPOP} this method will no longer
+ * cancel the notification. It will continue to cancel the notification for applications
+ * whose {@code targetSdkVersion} is earlier than {@link android.os.Build.VERSION_CODES#LOLLIPOP}.
+ */
+ @Deprecated
+ public final void cancelNotification(String pkg, String tag, int id) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().cancelNotificationFromListener(
+ mWrapper, pkg, tag, id);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+ /**
+ * Inform the notification manager about dismissal of a single notification.
+ * <p>
+ * Use this if your listener has a user interface that allows the user to dismiss individual
+ * notifications, similar to the behavior of Android's status bar and notification panel.
+ * It should be called after the user dismisses a single notification using your UI;
+ * upon being informed, the notification manager will actually remove the notification
+ * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback.
+ * <p>
+ * <b>Note:</b> If your listener allows the user to fire a notification's
+ * {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call
+ * this method at that time <i>if</i> the Notification in question has the
+ * {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set.
+ * <p>
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @param key Notification to dismiss from {@link StatusBarNotification#getKey()}.
+ */
+ public final void cancelNotification(String key) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().cancelNotificationsFromListener(mWrapper,
+ new String[] { key });
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+ /**
+ * Inform the notification manager about dismissal of all notifications.
+ * <p>
+ * Use this if your listener has a user interface that allows the user to dismiss all
+ * notifications, similar to the behavior of Android's status bar and notification panel.
+ * It should be called after the user invokes the "dismiss all" function of your UI;
+ * upon being informed, the notification manager will actually remove all active notifications
+ * and you will get multiple {@link #onNotificationRemoved(StatusBarNotification)} callbacks.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * {@see #cancelNotification(String, String, int)}
+ */
+ public final void cancelAllNotifications() {
+ cancelNotifications(null /*all*/);
+ }
+
+ /**
+ * Inform the notification manager about dismissal of specific notifications.
+ * <p>
+ * Use this if your listener has a user interface that allows the user to dismiss
+ * multiple notifications at once.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @param keys Notifications to dismiss, or {@code null} to dismiss all.
+ *
+ * {@see #cancelNotification(String, String, int)}
+ */
+ public final void cancelNotifications(String[] keys) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().cancelNotificationsFromListener(mWrapper, keys);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+ /**
+ * Inform the notification manager about snoozing a specific notification.
+ * <p>
+ * Use this if your listener has a user interface that allows the user to snooze a notification
+ * until a given {@link SnoozeCriterion}. It should be called after the user snoozes a single
+ * notification using your UI; upon being informed, the notification manager will actually
+ * remove the notification and you will get an
+ * {@link #onNotificationRemoved(StatusBarNotification)} callback. When the snoozing period
+ * expires, you will get a {@link #onNotificationPosted(StatusBarNotification, RankingMap)}
+ * callback for the notification.
+ * @param key The key of the notification to snooze
+ * @param snoozeCriterionId The{@link SnoozeCriterion#getId()} of a context to snooze the
+ * notification until.
+ * @hide
+ * @removed
+ */
+ @SystemApi
+ public final void snoozeNotification(String key, String snoozeCriterionId) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().snoozeNotificationUntilContextFromListener(
+ mWrapper, key, snoozeCriterionId);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+ /**
+ * Inform the notification manager about snoozing a specific notification.
+ * <p>
+ * Use this if your listener has a user interface that allows the user to snooze a notification
+ * for a time. It should be called after the user snoozes a single notification using
+ * your UI; upon being informed, the notification manager will actually remove the notification
+ * and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback. When the
+ * snoozing period expires, you will get a
+ * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the
+ * notification.
+ * @param key The key of the notification to snooze
+ * @param durationMs A duration to snooze the notification for, in milliseconds.
+ */
+ public final void snoozeNotification(String key, long durationMs) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().snoozeNotificationUntilFromListener(
+ mWrapper, key, durationMs);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+
+ /**
+ * Inform the notification manager that these notifications have been viewed by the
+ * user. This should only be called when there is sufficient confidence that the user is
+ * looking at the notifications, such as when the notifications appear on the screen due to
+ * an explicit user interaction.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @param keys Notifications to mark as seen.
+ */
+ public final void setNotificationsShown(String[] keys) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().setNotificationsShownFromListener(mWrapper, keys);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+
+ /**
+ * Updates a notification channel for a given package for a given user. This should only be used
+ * to reflect changes a user has made to the channel via the listener's user interface.
+ *
+ * <p>This method will throw a security exception if you don't have access to notifications
+ * for the given user.</p>
+ * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+ * device} in order to use this method.
+ *
+ * @param pkg The package the channel belongs to.
+ * @param user The user the channel belongs to.
+ * @param channel the channel to update.
+ */
+ public final void updateNotificationChannel(@NonNull String pkg, @NonNull UserHandle user,
+ @NonNull NotificationChannel channel) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().updateNotificationChannelFromPrivilegedListener(
+ mWrapper, pkg, user, channel);
+ } catch (RemoteException e) {
+ Log.v(TAG, "Unable to contact notification manager", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns all notification channels belonging to the given package for a given user.
+ *
+ * <p>This method will throw a security exception if you don't have access to notifications
+ * for the given user.</p>
+ * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+ * device} or be the {@link NotificationAssistantService notification assistant} in order to
+ * use this method.
+ *
+ * @param pkg The package to retrieve channels for.
+ */
+ public final List<NotificationChannel> getNotificationChannels(@NonNull String pkg,
+ @NonNull UserHandle user) {
+ if (!isBound()) return null;
+ try {
+
+ return getNotificationInterface().getNotificationChannelsFromPrivilegedListener(
+ mWrapper, pkg, user).getList();
+ } catch (RemoteException e) {
+ Log.v(TAG, "Unable to contact notification manager", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns all notification channel groups belonging to the given package for a given user.
+ *
+ * <p>This method will throw a security exception if you don't have access to notifications
+ * for the given user.</p>
+ * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated
+ * device} or be the {@link NotificationAssistantService notification assistant} in order to
+ * use this method.
+ *
+ * @param pkg The package to retrieve channel groups for.
+ */
+ public final List<NotificationChannelGroup> getNotificationChannelGroups(@NonNull String pkg,
+ @NonNull UserHandle user) {
+ if (!isBound()) return null;
+ try {
+
+ return getNotificationInterface().getNotificationChannelGroupsFromPrivilegedListener(
+ mWrapper, pkg, user).getList();
+ } catch (RemoteException e) {
+ Log.v(TAG, "Unable to contact notification manager", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the notification trim that will be received via {@link #onNotificationPosted}.
+ *
+ * <p>
+ * Setting a trim other than {@link #TRIM_FULL} enables listeners that don't need access to the
+ * full notification features right away to reduce their memory footprint. Full notifications
+ * can be requested on-demand via {@link #getActiveNotifications(int)}.
+ *
+ * <p>
+ * Set to {@link #TRIM_FULL} initially.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @hide
+ * @removed
+ *
+ * @param trim trim of the notifications to be passed via {@link #onNotificationPosted}.
+ * See <code>TRIM_*</code> constants.
+ */
+ @SystemApi
+ public final void setOnNotificationPostedTrim(int trim) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().setOnNotificationPostedTrimFromListener(mWrapper, trim);
+ } catch (RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+ /**
+ * Request the list of outstanding notifications (that is, those that are visible to the
+ * current user). Useful when you don't know what's already been posted.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @return An array of active notifications, sorted in natural order.
+ */
+ public StatusBarNotification[] getActiveNotifications() {
+ StatusBarNotification[] activeNotifications = getActiveNotifications(null, TRIM_FULL);
+ return activeNotifications != null ? activeNotifications : new StatusBarNotification[0];
+ }
+
+ /**
+ * Like {@link #getActiveNotifications()}, but returns the list of currently snoozed
+ * notifications, for all users this listener has access to.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @return An array of snoozed notifications, sorted in natural order.
+ */
+ public final StatusBarNotification[] getSnoozedNotifications() {
+ try {
+ ParceledListSlice<StatusBarNotification> parceledList = getNotificationInterface()
+ .getSnoozedNotificationsFromListener(mWrapper, TRIM_FULL);
+ return cleanUpNotificationList(parceledList);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ return null;
+ }
+
+ /**
+ * Request the list of outstanding notifications (that is, those that are visible to the
+ * current user). Useful when you don't know what's already been posted.
+ *
+ * @hide
+ * @removed
+ *
+ * @param trim trim of the notifications to be returned. See <code>TRIM_*</code> constants.
+ * @return An array of active notifications, sorted in natural order.
+ */
+ @SystemApi
+ public StatusBarNotification[] getActiveNotifications(int trim) {
+ StatusBarNotification[] activeNotifications = getActiveNotifications(null, trim);
+ return activeNotifications != null ? activeNotifications : new StatusBarNotification[0];
+ }
+
+ /**
+ * Request one or more notifications by key. Useful if you have been keeping track of
+ * notifications but didn't want to retain the bits, and now need to go back and extract
+ * more data out of those notifications.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @param keys the keys of the notifications to request
+ * @return An array of notifications corresponding to the requested keys, in the
+ * same order as the key list.
+ */
+ public StatusBarNotification[] getActiveNotifications(String[] keys) {
+ StatusBarNotification[] activeNotifications = getActiveNotifications(keys, TRIM_FULL);
+ return activeNotifications != null ? activeNotifications : new StatusBarNotification[0];
+ }
+
+ /**
+ * Request one or more notifications by key. Useful if you have been keeping track of
+ * notifications but didn't want to retain the bits, and now need to go back and extract
+ * more data out of those notifications.
+ *
+ * @hide
+ * @removed
+ *
+ * @param keys the keys of the notifications to request
+ * @param trim trim of the notifications to be returned. See <code>TRIM_*</code> constants.
+ * @return An array of notifications corresponding to the requested keys, in the
+ * same order as the key list.
+ */
+ @SystemApi
+ public StatusBarNotification[] getActiveNotifications(String[] keys, int trim) {
+ if (!isBound())
+ return null;
+ try {
+ ParceledListSlice<StatusBarNotification> parceledList = getNotificationInterface()
+ .getActiveNotificationsFromListener(mWrapper, keys, trim);
+ return cleanUpNotificationList(parceledList);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ return null;
+ }
+
+ private StatusBarNotification[] cleanUpNotificationList(
+ ParceledListSlice<StatusBarNotification> parceledList) {
+ if (parceledList == null || parceledList.getList() == null) {
+ return new StatusBarNotification[0];
+ }
+ List<StatusBarNotification> list = parceledList.getList();
+ ArrayList<StatusBarNotification> corruptNotifications = null;
+ int N = list.size();
+ for (int i = 0; i < N; i++) {
+ StatusBarNotification sbn = list.get(i);
+ Notification notification = sbn.getNotification();
+ try {
+ // convert icon metadata to legacy format for older clients
+ createLegacyIconExtras(notification);
+ // populate remote views for older clients.
+ maybePopulateRemoteViews(notification);
+ // populate people for older clients.
+ maybePopulatePeople(notification);
+ } catch (IllegalArgumentException e) {
+ if (corruptNotifications == null) {
+ corruptNotifications = new ArrayList<>(N);
+ }
+ corruptNotifications.add(sbn);
+ Log.w(TAG, "get(Active/Snoozed)Notifications: can't rebuild notification from " +
+ sbn.getPackageName());
+ }
+ }
+ if (corruptNotifications != null) {
+ list.removeAll(corruptNotifications);
+ }
+ return list.toArray(new StatusBarNotification[list.size()]);
+ }
+
+ /**
+ * Gets the set of hints representing current state.
+ *
+ * <p>
+ * The current state may differ from the requested state if the hint represents state
+ * shared across all listeners or a feature the notification host does not support or refuses
+ * to grant.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @return Zero or more of the HINT_ constants.
+ */
+ public final int getCurrentListenerHints() {
+ if (!isBound()) return 0;
+ try {
+ return getNotificationInterface().getHintsFromListener(mWrapper);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ return 0;
+ }
+ }
+
+ /**
+ * Gets the current notification interruption filter active on the host.
+ *
+ * <p>
+ * The interruption filter defines which notifications are allowed to interrupt the user
+ * (e.g. via sound & vibration) and is applied globally. Listeners can find out whether
+ * a specific notification matched the interruption filter via
+ * {@link Ranking#matchesInterruptionFilter()}.
+ * <p>
+ * The current filter may differ from the previously requested filter if the notification host
+ * does not support or refuses to apply the requested filter, or if another component changed
+ * the filter in the meantime.
+ * <p>
+ * Listen for updates using {@link #onInterruptionFilterChanged(int)}.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @return One of the INTERRUPTION_FILTER_ constants, or INTERRUPTION_FILTER_UNKNOWN when
+ * unavailable.
+ */
+ public final int getCurrentInterruptionFilter() {
+ if (!isBound()) return INTERRUPTION_FILTER_UNKNOWN;
+ try {
+ return getNotificationInterface().getInterruptionFilterFromListener(mWrapper);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ return INTERRUPTION_FILTER_UNKNOWN;
+ }
+ }
+
+ /**
+ * Clears listener hints set via {@link #getCurrentListenerHints()}.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ */
+ public final void clearRequestedListenerHints() {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().clearRequestedListenerHints(mWrapper);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+ /**
+ * Sets the desired {@link #getCurrentListenerHints() listener hints}.
+ *
+ * <p>
+ * This is merely a request, the host may or may not choose to take action depending
+ * on other listener requests or other global state.
+ * <p>
+ * Listen for updates using {@link #onListenerHintsChanged(int)}.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @param hints One or more of the HINT_ constants.
+ */
+ public final void requestListenerHints(int hints) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().requestHintsFromListener(mWrapper, hints);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+ /**
+ * Sets the desired {@link #getCurrentInterruptionFilter() interruption filter}.
+ *
+ * <p>
+ * This is merely a request, the host may or may not choose to apply the requested
+ * interruption filter depending on other listener requests or other global state.
+ * <p>
+ * Listen for updates using {@link #onInterruptionFilterChanged(int)}.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @param interruptionFilter One of the INTERRUPTION_FILTER_ constants.
+ */
+ public final void requestInterruptionFilter(int interruptionFilter) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface()
+ .requestInterruptionFilterFromListener(mWrapper, interruptionFilter);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
+ /**
+ * Returns current ranking information.
+ *
+ * <p>
+ * The returned object represents the current ranking snapshot and only
+ * applies for currently active notifications.
+ * <p>
+ * Generally you should use the RankingMap that is passed with events such
+ * as {@link #onNotificationPosted(StatusBarNotification, RankingMap)},
+ * {@link #onNotificationRemoved(StatusBarNotification, RankingMap)}, and
+ * so on. This method should only be used when needing access outside of
+ * such events, for example to retrieve the RankingMap right after
+ * initialization.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation.
+ *
+ * @return A {@link RankingMap} object providing access to ranking information
+ */
+ public RankingMap getCurrentRanking() {
+ synchronized (mLock) {
+ return mRankingMap;
+ }
+ }
+
+ /**
+ * This is not the lifecycle event you are looking for.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing any operations.
+ */
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (mWrapper == null) {
+ mWrapper = new NotificationListenerWrapper();
+ }
+ return mWrapper;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ protected boolean isBound() {
+ if (mWrapper == null) {
+ Log.w(TAG, "Notification listener service not yet bound.");
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onDestroy() {
+ onListenerDisconnected();
+ super.onDestroy();
+ }
+
+ /**
+ * Directly register this service with the Notification Manager.
+ *
+ * <p>Only system services may use this call. It will fail for non-system callers.
+ * Apps should ask the user to add their listener in Settings.
+ *
+ * @param context Context required for accessing resources. Since this service isn't
+ * launched as a real Service when using this method, a context has to be passed in.
+ * @param componentName the component that will consume the notification information
+ * @param currentUser the user to use as the stream filter
+ * @hide
+ * @removed
+ */
+ @SystemApi
+ public void registerAsSystemService(Context context, ComponentName componentName,
+ int currentUser) throws RemoteException {
+ if (mWrapper == null) {
+ mWrapper = new NotificationListenerWrapper();
+ }
+ mSystemContext = context;
+ INotificationManager noMan = getNotificationInterface();
+ mHandler = new MyHandler(context.getMainLooper());
+ mCurrentUser = currentUser;
+ noMan.registerListener(mWrapper, componentName, currentUser);
+ }
+
+ /**
+ * Directly unregister this service from the Notification Manager.
+ *
+ * <p>This method will fail for listeners that were not registered
+ * with (@link registerAsService).
+ * @hide
+ * @removed
+ */
+ @SystemApi
+ public void unregisterAsSystemService() throws RemoteException {
+ if (mWrapper != null) {
+ INotificationManager noMan = getNotificationInterface();
+ noMan.unregisterListener(mWrapper, mCurrentUser);
+ }
+ }
+
+ /**
+ * Request that the listener be rebound, after a previous call to {@link #requestUnbind}.
+ *
+ * <p>This method will fail for listeners that have
+ * not been granted the permission by the user.
+ */
+ public static void requestRebind(ComponentName componentName) {
+ INotificationManager noMan = INotificationManager.Stub.asInterface(
+ ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+ try {
+ noMan.requestBindListener(componentName);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Request that the service be unbound.
+ *
+ * <p>Once this is called, you will no longer receive updates and no method calls are
+ * guaranteed to be successful, until you next receive the {@link #onListenerConnected()} event.
+ * The service will likely be killed by the system after this call.
+ *
+ * <p>The service should wait for the {@link #onListenerConnected()} event
+ * before performing this operation. I know it's tempting, but you must wait.
+ */
+ public final void requestUnbind() {
+ if (mWrapper != null) {
+ INotificationManager noMan = getNotificationInterface();
+ try {
+ noMan.requestUnbindListener(mWrapper);
+ // Disable future messages.
+ isConnected = false;
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Convert new-style Icons to legacy representations for pre-M clients.
+ * @hide
+ */
+ public final void createLegacyIconExtras(Notification n) {
+ if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.M) {
+ Icon smallIcon = n.getSmallIcon();
+ Icon largeIcon = n.getLargeIcon();
+ if (smallIcon != null && smallIcon.getType() == Icon.TYPE_RESOURCE) {
+ n.extras.putInt(Notification.EXTRA_SMALL_ICON, smallIcon.getResId());
+ n.icon = smallIcon.getResId();
+ }
+ if (largeIcon != null) {
+ Drawable d = largeIcon.loadDrawable(getContext());
+ if (d != null && d instanceof BitmapDrawable) {
+ final Bitmap largeIconBits = ((BitmapDrawable) d).getBitmap();
+ n.extras.putParcelable(Notification.EXTRA_LARGE_ICON, largeIconBits);
+ n.largeIcon = largeIconBits;
+ }
+ }
+ }
+ }
+
+ /**
+ * Populates remote views for pre-N targeting apps.
+ */
+ private void maybePopulateRemoteViews(Notification notification) {
+ if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
+ Builder builder = Builder.recoverBuilder(getContext(), notification);
+
+ // Some styles wrap Notification's contentView, bigContentView and headsUpContentView.
+ // First inflate them all, only then set them to avoid recursive wrapping.
+ RemoteViews content = builder.createContentView();
+ RemoteViews big = builder.createBigContentView();
+ RemoteViews headsUp = builder.createHeadsUpContentView();
+
+ notification.contentView = content;
+ notification.bigContentView = big;
+ notification.headsUpContentView = headsUp;
+ }
+ }
+
+ /**
+ * Populates remote views for pre-P targeting apps.
+ */
+ private void maybePopulatePeople(Notification notification) {
+ if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P) {
+ ArrayList<Person> people = notification.extras.getParcelableArrayList(
+ Notification.EXTRA_PEOPLE_LIST);
+ if (people != null && people.isEmpty()) {
+ int size = people.size();
+ String[] peopleArray = new String[size];
+ for (int i = 0; i < size; i++) {
+ Person person = people.get(i);
+ peopleArray[i] = person.resolveToLegacyUri();
+ }
+ notification.extras.putStringArray(Notification.EXTRA_PEOPLE, peopleArray);
+ }
+ }
+ }
+
+ /** @hide */
+ protected class NotificationListenerWrapper extends INotificationListener.Stub {
+ @Override
+ public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
+ NotificationRankingUpdate update) {
+ StatusBarNotification sbn;
+ try {
+ sbn = sbnHolder.get();
+ } catch (RemoteException e) {
+ Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e);
+ return;
+ }
+ if (sbn == null) {
+ Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification");
+ return;
+ }
+
+ try {
+ // convert icon metadata to legacy format for older clients
+ createLegacyIconExtras(sbn.getNotification());
+ maybePopulateRemoteViews(sbn.getNotification());
+ maybePopulatePeople(sbn.getNotification());
+ } catch (IllegalArgumentException e) {
+ // warn and drop corrupt notification
+ Log.w(TAG, "onNotificationPosted: can't rebuild notification from " +
+ sbn.getPackageName());
+ sbn = null;
+ }
+
+ // protect subclass from concurrent modifications of (@link mNotificationKeys}.
+ synchronized (mLock) {
+ applyUpdateLocked(update);
+ if (sbn != null) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = sbn;
+ args.arg2 = mRankingMap;
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_POSTED,
+ args).sendToTarget();
+ } else {
+ // still pass along the ranking map, it may contain other information
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_RANKING_UPDATE,
+ mRankingMap).sendToTarget();
+ }
+ }
+
+ }
+
+ @Override
+ public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder,
+ NotificationRankingUpdate update, NotificationStats stats, int reason) {
+ StatusBarNotification sbn;
+ try {
+ sbn = sbnHolder.get();
+ } catch (RemoteException e) {
+ Log.w(TAG, "onNotificationRemoved: Error receiving StatusBarNotification", e);
+ return;
+ }
+ if (sbn == null) {
+ Log.w(TAG, "onNotificationRemoved: Error receiving StatusBarNotification");
+ return;
+ }
+ // protect subclass from concurrent modifications of (@link mNotificationKeys}.
+ synchronized (mLock) {
+ applyUpdateLocked(update);
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = sbn;
+ args.arg2 = mRankingMap;
+ args.arg3 = reason;
+ args.arg4 = stats;
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_REMOVED,
+ args).sendToTarget();
+ }
+
+ }
+
+ @Override
+ public void onListenerConnected(NotificationRankingUpdate update) {
+ // protect subclass from concurrent modifications of (@link mNotificationKeys}.
+ synchronized (mLock) {
+ applyUpdateLocked(update);
+ }
+ isConnected = true;
+ mHandler.obtainMessage(MyHandler.MSG_ON_LISTENER_CONNECTED).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationRankingUpdate(NotificationRankingUpdate update)
+ throws RemoteException {
+ // protect subclass from concurrent modifications of (@link mNotificationKeys}.
+ synchronized (mLock) {
+ applyUpdateLocked(update);
+ mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_RANKING_UPDATE,
+ mRankingMap).sendToTarget();
+ }
+
+ }
+
+ @Override
+ public void onListenerHintsChanged(int hints) throws RemoteException {
+ mHandler.obtainMessage(MyHandler.MSG_ON_LISTENER_HINTS_CHANGED,
+ hints, 0).sendToTarget();
+ }
+
+ @Override
+ public void onInterruptionFilterChanged(int interruptionFilter) throws RemoteException {
+ mHandler.obtainMessage(MyHandler.MSG_ON_INTERRUPTION_FILTER_CHANGED,
+ interruptionFilter, 0).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationEnqueuedWithChannel(
+ IStatusBarNotificationHolder notificationHolder, NotificationChannel channel)
+ throws RemoteException {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onNotificationsSeen(List<String> keys)
+ throws RemoteException {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onPanelRevealed(int items) throws RemoteException {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onPanelHidden() throws RemoteException {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onNotificationVisibilityChanged(
+ String key, boolean isVisible) {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onNotificationSnoozedUntilContext(
+ IStatusBarNotificationHolder notificationHolder, String snoozeCriterionId)
+ throws RemoteException {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onNotificationExpansionChanged(
+ String key, boolean isUserAction, boolean isExpanded) {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onNotificationDirectReply(String key) {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onSuggestedReplySent(String key, CharSequence reply, int source) {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onActionClicked(String key, Notification.Action action, int source) {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onAllowedAdjustmentsChanged() {
+ // no-op in the listener
+ }
+
+ @Override
+ public void onNotificationChannelModification(String pkgName, UserHandle user,
+ NotificationChannel channel,
+ @ChannelOrGroupModificationTypes int modificationType) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = pkgName;
+ args.arg2 = user;
+ args.arg3 = channel;
+ args.arg4 = modificationType;
+ mHandler.obtainMessage(
+ MyHandler.MSG_ON_NOTIFICATION_CHANNEL_MODIFIED, args).sendToTarget();
+ }
+
+ @Override
+ public void onNotificationChannelGroupModification(String pkgName, UserHandle user,
+ NotificationChannelGroup group,
+ @ChannelOrGroupModificationTypes int modificationType) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = pkgName;
+ args.arg2 = user;
+ args.arg3 = group;
+ args.arg4 = modificationType;
+ mHandler.obtainMessage(
+ MyHandler.MSG_ON_NOTIFICATION_CHANNEL_GROUP_MODIFIED, args).sendToTarget();
+ }
+
+ @Override
+ public void onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons) {
+ mHandler.obtainMessage(MyHandler.MSG_ON_STATUS_BAR_ICON_BEHAVIOR_CHANGED,
+ hideSilentStatusIcons).sendToTarget();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @GuardedBy("mLock")
+ public final void applyUpdateLocked(NotificationRankingUpdate update) {
+ mRankingMap = update.getRankingMap();
+ }
+
+ /** @hide */
+ protected Context getContext() {
+ if (mSystemContext != null) {
+ return mSystemContext;
+ }
+ return this;
+ }
+
+ /**
+ * Stores ranking related information on a currently active notification.
+ *
+ * <p>
+ * Ranking objects aren't automatically updated as notification events
+ * occur. Instead, ranking information has to be retrieved again via the
+ * current {@link RankingMap}.
+ */
+ public static class Ranking {
+
+ /** Value signifying that the user has not expressed a per-app visibility override value.
+ * @hide */
+ public static final int VISIBILITY_NO_OVERRIDE = NotificationManager.VISIBILITY_NO_OVERRIDE;
+
+ /**
+ * The user is likely to have a negative reaction to this notification.
+ */
+ public static final int USER_SENTIMENT_NEGATIVE = -1;
+ /**
+ * It is not known how the user will react to this notification.
+ */
+ public static final int USER_SENTIMENT_NEUTRAL = 0;
+ /**
+ * The user is likely to have a positive reaction to this notification.
+ */
+ public static final int USER_SENTIMENT_POSITIVE = 1;
+
+ /** @hide */
+ @IntDef(prefix = { "USER_SENTIMENT_" }, value = {
+ USER_SENTIMENT_NEGATIVE, USER_SENTIMENT_NEUTRAL, USER_SENTIMENT_POSITIVE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface UserSentiment {}
+
+ private @NonNull String mKey;
+ private int mRank = -1;
+ private boolean mIsAmbient;
+ private boolean mMatchesInterruptionFilter;
+ private int mVisibilityOverride;
+ private int mSuppressedVisualEffects;
+ private @NotificationManager.Importance int mImportance;
+ private CharSequence mImportanceExplanation;
+ // System specified group key.
+ private String mOverrideGroupKey;
+ // Notification assistant channel override.
+ private NotificationChannel mChannel;
+ // Notification assistant people override.
+ private ArrayList<String> mOverridePeople;
+ // Notification assistant snooze criteria.
+ private ArrayList<SnoozeCriterion> mSnoozeCriteria;
+ private boolean mShowBadge;
+ private @UserSentiment int mUserSentiment = USER_SENTIMENT_NEUTRAL;
+ private boolean mHidden;
+ private long mLastAudiblyAlertedMs;
+ private boolean mNoisy;
+ private ArrayList<Notification.Action> mSmartActions;
+ private ArrayList<CharSequence> mSmartReplies;
+ private boolean mCanBubble;
+ private boolean mVisuallyInterruptive;
+ private boolean mIsConversation;
+ private ShortcutInfo mShortcutInfo;
+ private boolean mIsBubble;
+
+ private static final int PARCEL_VERSION = 2;
+
+ public Ranking() { }
+
+ // You can parcel it, but it's not Parcelable
+ /** @hide */
+ @VisibleForTesting
+ public void writeToParcel(Parcel out, int flags) {
+ final long start = out.dataPosition();
+ out.writeInt(PARCEL_VERSION);
+ out.writeString(mKey);
+ out.writeInt(mRank);
+ out.writeBoolean(mIsAmbient);
+ out.writeBoolean(mMatchesInterruptionFilter);
+ out.writeInt(mVisibilityOverride);
+ out.writeInt(mSuppressedVisualEffects);
+ out.writeInt(mImportance);
+ out.writeCharSequence(mImportanceExplanation);
+ out.writeString(mOverrideGroupKey);
+ out.writeParcelable(mChannel, flags);
+ out.writeStringList(mOverridePeople);
+ out.writeTypedList(mSnoozeCriteria, flags);
+ out.writeBoolean(mShowBadge);
+ out.writeInt(mUserSentiment);
+ out.writeBoolean(mHidden);
+ out.writeLong(mLastAudiblyAlertedMs);
+ out.writeBoolean(mNoisy);
+ out.writeTypedList(mSmartActions, flags);
+ out.writeCharSequenceList(mSmartReplies);
+ out.writeBoolean(mCanBubble);
+ out.writeBoolean(mVisuallyInterruptive);
+ out.writeBoolean(mIsConversation);
+ out.writeParcelable(mShortcutInfo, flags);
+ out.writeBoolean(mIsBubble);
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public Ranking(Parcel in) {
+ final ClassLoader cl = getClass().getClassLoader();
+
+ final int version = in.readInt();
+ if (version != PARCEL_VERSION) {
+ throw new IllegalArgumentException("malformed Ranking parcel: " + in + " version "
+ + version + ", expected " + PARCEL_VERSION);
+ }
+ mKey = in.readString();
+ mRank = in.readInt();
+ mIsAmbient = in.readBoolean();
+ mMatchesInterruptionFilter = in.readBoolean();
+ mVisibilityOverride = in.readInt();
+ mSuppressedVisualEffects = in.readInt();
+ mImportance = in.readInt();
+ mImportanceExplanation = in.readCharSequence(); // may be null
+ mOverrideGroupKey = in.readString(); // may be null
+ mChannel = in.readParcelable(cl); // may be null
+ mOverridePeople = in.createStringArrayList();
+ mSnoozeCriteria = in.createTypedArrayList(SnoozeCriterion.CREATOR);
+ mShowBadge = in.readBoolean();
+ mUserSentiment = in.readInt();
+ mHidden = in.readBoolean();
+ mLastAudiblyAlertedMs = in.readLong();
+ mNoisy = in.readBoolean();
+ mSmartActions = in.createTypedArrayList(Notification.Action.CREATOR);
+ mSmartReplies = in.readCharSequenceList();
+ mCanBubble = in.readBoolean();
+ mVisuallyInterruptive = in.readBoolean();
+ mIsConversation = in.readBoolean();
+ mShortcutInfo = in.readParcelable(cl);
+ mIsBubble = in.readBoolean();
+ }
+
+
+ /**
+ * Returns the key of the notification this Ranking applies to.
+ */
+ public String getKey() {
+ return mKey;
+ }
+
+ /**
+ * Returns the rank of the notification.
+ *
+ * @return the rank of the notification, that is the 0-based index in
+ * the list of active notifications.
+ */
+ public int getRank() {
+ return mRank;
+ }
+
+ /**
+ * Returns whether the notification is an ambient notification, that is
+ * a notification that doesn't require the user's immediate attention.
+ */
+ public boolean isAmbient() {
+ return mIsAmbient;
+ }
+
+ /**
+ * Returns the user specified visibility for the package that posted
+ * this notification, or
+ * {@link NotificationListenerService.Ranking#VISIBILITY_NO_OVERRIDE} if
+ * no such preference has been expressed.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public int getVisibilityOverride() {
+ return mVisibilityOverride;
+ }
+
+ /**
+ * Returns the type(s) of visual effects that should be suppressed for this notification.
+ * See {@link NotificationManager.Policy}, e.g.
+ * {@link NotificationManager.Policy#SUPPRESSED_EFFECT_LIGHTS}.
+ */
+ public int getSuppressedVisualEffects() {
+ return mSuppressedVisualEffects;
+ }
+
+ /**
+ * Returns whether the notification matches the user's interruption
+ * filter.
+ *
+ * @return {@code true} if the notification is allowed by the filter, or
+ * {@code false} if it is blocked.
+ */
+ public boolean matchesInterruptionFilter() {
+ return mMatchesInterruptionFilter;
+ }
+
+ /**
+ * Returns the importance of the notification, which dictates its
+ * modes of presentation, see: {@link NotificationManager#IMPORTANCE_DEFAULT}, etc.
+ *
+ * @return the importance of the notification
+ */
+ public @NotificationManager.Importance int getImportance() {
+ return mImportance;
+ }
+
+ /**
+ * If the importance has been overridden by user preference, then this will be non-null,
+ * and should be displayed to the user.
+ *
+ * @return the explanation for the importance, or null if it is the natural importance
+ */
+ public CharSequence getImportanceExplanation() {
+ return mImportanceExplanation;
+ }
+
+ /**
+ * If the system has overridden the group key, then this will be non-null, and this
+ * key should be used to bundle notifications.
+ */
+ public String getOverrideGroupKey() {
+ return mOverrideGroupKey;
+ }
+
+ /**
+ * Returns the notification channel this notification was posted to, which dictates
+ * notification behavior and presentation.
+ */
+ public NotificationChannel getChannel() {
+ return mChannel;
+ }
+
+ /**
+ * Returns how the system thinks the user feels about notifications from the
+ * channel provided by {@link #getChannel()}. You can use this information to expose
+ * controls to help the user block this channel's notifications, if the sentiment is
+ * {@link #USER_SENTIMENT_NEGATIVE}, or emphasize this notification if the sentiment is
+ * {@link #USER_SENTIMENT_POSITIVE}.
+ */
+ public int getUserSentiment() {
+ return mUserSentiment;
+ }
+
+ /**
+ * If the {@link NotificationAssistantService} has added people to this notification, then
+ * this will be non-null.
+ * @hide
+ * @removed
+ */
+ @SystemApi
+ public List<String> getAdditionalPeople() {
+ return mOverridePeople;
+ }
+
+ /**
+ * Returns snooze criteria provided by the {@link NotificationAssistantService}. If your
+ * user interface displays options for snoozing notifications these criteria should be
+ * displayed as well.
+ * @hide
+ * @removed
+ */
+ @SystemApi
+ public List<SnoozeCriterion> getSnoozeCriteria() {
+ return mSnoozeCriteria;
+ }
+
+ /**
+ * Returns a list of smart {@link Notification.Action} that can be added by the
+ * {@link NotificationAssistantService}
+ */
+ public @NonNull List<Notification.Action> getSmartActions() {
+ return mSmartActions;
+ }
+
+ /**
+ * Returns a list of smart replies that can be added by the
+ * {@link NotificationAssistantService}
+ */
+ public @NonNull List<CharSequence> getSmartReplies() {
+ return mSmartReplies;
+ }
+
+ /**
+ * Returns whether this notification can be displayed as a badge.
+ *
+ * @return true if the notification can be displayed as a badge, false otherwise.
+ */
+ public boolean canShowBadge() {
+ return mShowBadge;
+ }
+
+ /**
+ * Returns whether the app that posted this notification is suspended, so this notification
+ * should be hidden.
+ *
+ * @return true if the notification should be hidden, false otherwise.
+ */
+ public boolean isSuspended() {
+ return mHidden;
+ }
+
+ /**
+ * Returns the last time this notification alerted the user via sound or vibration.
+ *
+ * @return the time of the last alerting behavior, in milliseconds.
+ */
+ @CurrentTimeMillisLong
+ public long getLastAudiblyAlertedMillis() {
+ return mLastAudiblyAlertedMs;
+ }
+
+ /**
+ * Returns whether the user has allowed bubbles globally, at the app level, and at the
+ * channel level for this notification.
+ *
+ * <p>This does not take into account the current importance of the notification, the
+ * current DND state, or whether the posting app is foreground.</p>
+ */
+ public boolean canBubble() {
+ return mCanBubble;
+ }
+
+ /** @hide */
+ public boolean visuallyInterruptive() {
+ return mVisuallyInterruptive;
+ }
+
+ /** @hide */
+ public boolean isNoisy() {
+ return mNoisy;
+ }
+
+ /**
+ * Returns whether this notification is a conversation notification.
+ * @hide
+ */
+ public boolean isConversation() {
+ return mIsConversation;
+ }
+
+ /**
+ * Returns whether this notification is actively a bubble.
+ * @hide
+ */
+ public boolean isBubble() {
+ return mIsBubble;
+ }
+
+ /**
+ * @hide
+ */
+ public @Nullable ShortcutInfo getShortcutInfo() {
+ return mShortcutInfo;
+ }
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public void populate(String key, int rank, boolean matchesInterruptionFilter,
+ int visibilityOverride, int suppressedVisualEffects, int importance,
+ CharSequence explanation, String overrideGroupKey,
+ NotificationChannel channel, ArrayList<String> overridePeople,
+ ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge,
+ int userSentiment, boolean hidden, long lastAudiblyAlertedMs,
+ boolean noisy, ArrayList<Notification.Action> smartActions,
+ ArrayList<CharSequence> smartReplies, boolean canBubble,
+ boolean visuallyInterruptive, boolean isConversation, ShortcutInfo shortcutInfo,
+ boolean isBubble) {
+ mKey = key;
+ mRank = rank;
+ mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW;
+ mMatchesInterruptionFilter = matchesInterruptionFilter;
+ mVisibilityOverride = visibilityOverride;
+ mSuppressedVisualEffects = suppressedVisualEffects;
+ mImportance = importance;
+ mImportanceExplanation = explanation;
+ mOverrideGroupKey = overrideGroupKey;
+ mChannel = channel;
+ mOverridePeople = overridePeople;
+ mSnoozeCriteria = snoozeCriteria;
+ mShowBadge = showBadge;
+ mUserSentiment = userSentiment;
+ mHidden = hidden;
+ mLastAudiblyAlertedMs = lastAudiblyAlertedMs;
+ mNoisy = noisy;
+ mSmartActions = smartActions;
+ mSmartReplies = smartReplies;
+ mCanBubble = canBubble;
+ mVisuallyInterruptive = visuallyInterruptive;
+ mIsConversation = isConversation;
+ mShortcutInfo = shortcutInfo;
+ mIsBubble = isBubble;
+ }
+
+ /**
+ * @hide
+ */
+ public void populate(Ranking other) {
+ populate(other.mKey,
+ other.mRank,
+ other.mMatchesInterruptionFilter,
+ other.mVisibilityOverride,
+ other.mSuppressedVisualEffects,
+ other.mImportance,
+ other.mImportanceExplanation,
+ other.mOverrideGroupKey,
+ other.mChannel,
+ other.mOverridePeople,
+ other.mSnoozeCriteria,
+ other.mShowBadge,
+ other.mUserSentiment,
+ other.mHidden,
+ other.mLastAudiblyAlertedMs,
+ other.mNoisy,
+ other.mSmartActions,
+ other.mSmartReplies,
+ other.mCanBubble,
+ other.mVisuallyInterruptive,
+ other.mIsConversation,
+ other.mShortcutInfo,
+ other.mIsBubble);
+ }
+
+ /**
+ * {@hide}
+ */
+ public static String importanceToString(int importance) {
+ switch (importance) {
+ case NotificationManager.IMPORTANCE_UNSPECIFIED:
+ return "UNSPECIFIED";
+ case NotificationManager.IMPORTANCE_NONE:
+ return "NONE";
+ case NotificationManager.IMPORTANCE_MIN:
+ return "MIN";
+ case NotificationManager.IMPORTANCE_LOW:
+ return "LOW";
+ case NotificationManager.IMPORTANCE_DEFAULT:
+ return "DEFAULT";
+ case NotificationManager.IMPORTANCE_HIGH:
+ case NotificationManager.IMPORTANCE_MAX:
+ return "HIGH";
+ default:
+ return "UNKNOWN(" + String.valueOf(importance) + ")";
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Ranking other = (Ranking) o;
+ return Objects.equals(mKey, other.mKey)
+ && Objects.equals(mRank, other.mRank)
+ && Objects.equals(mMatchesInterruptionFilter, other.mMatchesInterruptionFilter)
+ && Objects.equals(mVisibilityOverride, other.mVisibilityOverride)
+ && Objects.equals(mSuppressedVisualEffects, other.mSuppressedVisualEffects)
+ && Objects.equals(mImportance, other.mImportance)
+ && Objects.equals(mImportanceExplanation, other.mImportanceExplanation)
+ && Objects.equals(mOverrideGroupKey, other.mOverrideGroupKey)
+ && Objects.equals(mChannel, other.mChannel)
+ && Objects.equals(mOverridePeople, other.mOverridePeople)
+ && Objects.equals(mSnoozeCriteria, other.mSnoozeCriteria)
+ && Objects.equals(mShowBadge, other.mShowBadge)
+ && Objects.equals(mUserSentiment, other.mUserSentiment)
+ && Objects.equals(mHidden, other.mHidden)
+ && Objects.equals(mLastAudiblyAlertedMs, other.mLastAudiblyAlertedMs)
+ && Objects.equals(mNoisy, other.mNoisy)
+ // Action.equals() doesn't exist so let's just compare list lengths
+ && ((mSmartActions == null ? 0 : mSmartActions.size())
+ == (other.mSmartActions == null ? 0 : other.mSmartActions.size()))
+ && Objects.equals(mSmartReplies, other.mSmartReplies)
+ && Objects.equals(mCanBubble, other.mCanBubble)
+ && Objects.equals(mVisuallyInterruptive, other.mVisuallyInterruptive)
+ && Objects.equals(mIsConversation, other.mIsConversation)
+ // Shortcutinfo doesn't have equals either; use id
+ && Objects.equals((mShortcutInfo == null ? 0 : mShortcutInfo.getId()),
+ (other.mShortcutInfo == null ? 0 : other.mShortcutInfo.getId()))
+ && Objects.equals(mIsBubble, other.mIsBubble);
+ }
+ }
+
+ /**
+ * Provides access to ranking information on currently active
+ * notifications.
+ *
+ * <p>
+ * Note that this object represents a ranking snapshot that only applies to
+ * notifications active at the time of retrieval.
+ */
+ public static class RankingMap implements Parcelable {
+ private ArrayList<String> mOrderedKeys = new ArrayList<>();
+ // Note: all String keys should be intern'd as pointers into mOrderedKeys
+ private ArrayMap<String, Ranking> mRankings = new ArrayMap<>();
+
+ /**
+ * @hide
+ */
+ public RankingMap(Ranking[] rankings) {
+ for (int i = 0; i < rankings.length; i++) {
+ final String key = rankings[i].getKey();
+ mOrderedKeys.add(key);
+ mRankings.put(key, rankings[i]);
+ }
+ }
+
+ // -- parcelable interface --
+
+ private RankingMap(Parcel in) {
+ final ClassLoader cl = getClass().getClassLoader();
+ final int count = in.readInt();
+ mOrderedKeys.ensureCapacity(count);
+ mRankings.ensureCapacity(count);
+ for (int i = 0; i < count; i++) {
+ final Ranking r = new Ranking(in);
+ final String key = r.getKey();
+ mOrderedKeys.add(key);
+ mRankings.put(key, r);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ RankingMap other = (RankingMap) o;
+
+ return mOrderedKeys.equals(other.mOrderedKeys)
+ && mRankings.equals(other.mRankings);
+
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ final int count = mOrderedKeys.size();
+ out.writeInt(count);
+ for (int i = 0; i < count; i++) {
+ mRankings.get(mOrderedKeys.get(i)).writeToParcel(out, flags);
+ }
+ }
+
+ public static final @android.annotation.NonNull Creator<RankingMap> CREATOR = new Creator<RankingMap>() {
+ @Override
+ public RankingMap createFromParcel(Parcel source) {
+ return new RankingMap(source);
+ }
+
+ @Override
+ public RankingMap[] newArray(int size) {
+ return new RankingMap[size];
+ }
+ };
+
+ /**
+ * Request the list of notification keys in their current ranking
+ * order.
+ *
+ * @return An array of active notification keys, in their ranking order.
+ */
+ public String[] getOrderedKeys() {
+ return mOrderedKeys.toArray(new String[0]);
+ }
+
+ /**
+ * Populates outRanking with ranking information for the notification
+ * with the given key.
+ *
+ * @return true if a valid key has been passed and outRanking has
+ * been populated; false otherwise
+ */
+ public boolean getRanking(String key, Ranking outRanking) {
+ if (mRankings.containsKey(key)) {
+ outRanking.populate(mRankings.get(key));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get a reference to the actual Ranking object corresponding to the key.
+ * Used only by unit tests.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public Ranking getRawRankingObject(String key) {
+ return mRankings.get(key);
+ }
+ }
+
+ private final class MyHandler extends Handler {
+ public static final int MSG_ON_NOTIFICATION_POSTED = 1;
+ public static final int MSG_ON_NOTIFICATION_REMOVED = 2;
+ public static final int MSG_ON_LISTENER_CONNECTED = 3;
+ public static final int MSG_ON_NOTIFICATION_RANKING_UPDATE = 4;
+ public static final int MSG_ON_LISTENER_HINTS_CHANGED = 5;
+ public static final int MSG_ON_INTERRUPTION_FILTER_CHANGED = 6;
+ public static final int MSG_ON_NOTIFICATION_CHANNEL_MODIFIED = 7;
+ public static final int MSG_ON_NOTIFICATION_CHANNEL_GROUP_MODIFIED = 8;
+ public static final int MSG_ON_STATUS_BAR_ICON_BEHAVIOR_CHANGED = 9;
+
+ public MyHandler(Looper looper) {
+ super(looper, null, false);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (!isConnected) {
+ return;
+ }
+ switch (msg.what) {
+ case MSG_ON_NOTIFICATION_POSTED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ StatusBarNotification sbn = (StatusBarNotification) args.arg1;
+ RankingMap rankingMap = (RankingMap) args.arg2;
+ args.recycle();
+ onNotificationPosted(sbn, rankingMap);
+ } break;
+
+ case MSG_ON_NOTIFICATION_REMOVED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ StatusBarNotification sbn = (StatusBarNotification) args.arg1;
+ RankingMap rankingMap = (RankingMap) args.arg2;
+ int reason = (int) args.arg3;
+ NotificationStats stats = (NotificationStats) args.arg4;
+ args.recycle();
+ onNotificationRemoved(sbn, rankingMap, stats, reason);
+ } break;
+
+ case MSG_ON_LISTENER_CONNECTED: {
+ onListenerConnected();
+ } break;
+
+ case MSG_ON_NOTIFICATION_RANKING_UPDATE: {
+ RankingMap rankingMap = (RankingMap) msg.obj;
+ onNotificationRankingUpdate(rankingMap);
+ } break;
+
+ case MSG_ON_LISTENER_HINTS_CHANGED: {
+ final int hints = msg.arg1;
+ onListenerHintsChanged(hints);
+ } break;
+
+ case MSG_ON_INTERRUPTION_FILTER_CHANGED: {
+ final int interruptionFilter = msg.arg1;
+ onInterruptionFilterChanged(interruptionFilter);
+ } break;
+
+ case MSG_ON_NOTIFICATION_CHANNEL_MODIFIED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ String pkgName = (String) args.arg1;
+ UserHandle user= (UserHandle) args.arg2;
+ NotificationChannel channel = (NotificationChannel) args.arg3;
+ int modificationType = (int) args.arg4;
+ onNotificationChannelModified(pkgName, user, channel, modificationType);
+ } break;
+
+ case MSG_ON_NOTIFICATION_CHANNEL_GROUP_MODIFIED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ String pkgName = (String) args.arg1;
+ UserHandle user = (UserHandle) args.arg2;
+ NotificationChannelGroup group = (NotificationChannelGroup) args.arg3;
+ int modificationType = (int) args.arg4;
+ onNotificationChannelGroupModified(pkgName, user, group, modificationType);
+ } break;
+
+ case MSG_ON_STATUS_BAR_ICON_BEHAVIOR_CHANGED: {
+ onSilentStatusBarIconsVisibilityChanged((Boolean) msg.obj);
+ } break;
+ }
+ }
+ }
+}
diff --git a/android/service/notification/NotificationRankingUpdate.java b/android/service/notification/NotificationRankingUpdate.java
new file mode 100644
index 0000000..675c5cd
--- /dev/null
+++ b/android/service/notification/NotificationRankingUpdate.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2014 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.notification;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * @hide
+ */
+public class NotificationRankingUpdate implements Parcelable {
+ private final NotificationListenerService.RankingMap mRankingMap;
+
+ public NotificationRankingUpdate(NotificationListenerService.Ranking[] rankings) {
+ mRankingMap = new NotificationListenerService.RankingMap(rankings);
+ }
+
+ public NotificationRankingUpdate(Parcel in) {
+ mRankingMap = in.readParcelable(getClass().getClassLoader());
+ }
+
+ public NotificationListenerService.RankingMap getRankingMap() {
+ return mRankingMap;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ NotificationRankingUpdate other = (NotificationRankingUpdate) o;
+ return mRankingMap.equals(other.mRankingMap);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(mRankingMap, flags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<NotificationRankingUpdate> CREATOR
+ = new Parcelable.Creator<NotificationRankingUpdate>() {
+ public NotificationRankingUpdate createFromParcel(Parcel parcel) {
+ return new NotificationRankingUpdate(parcel);
+ }
+
+ public NotificationRankingUpdate[] newArray(int size) {
+ return new NotificationRankingUpdate[size];
+ }
+ };
+}
diff --git a/android/service/notification/NotificationStats.java b/android/service/notification/NotificationStats.java
new file mode 100644
index 0000000..8be114c
--- /dev/null
+++ b/android/service/notification/NotificationStats.java
@@ -0,0 +1,312 @@
+/**
+ * Copyright (C) 2017 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.notification;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.RemoteInput;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Information about how the user has interacted with a given notification.
+ * @hide
+ */
+@TestApi
+@SystemApi
+public final class NotificationStats implements Parcelable {
+
+ private boolean mSeen;
+ private boolean mExpanded;
+ private boolean mDirectReplied;
+ private boolean mSnoozed;
+ private boolean mViewedSettings;
+ private boolean mInteracted;
+
+ /** @hide */
+ @IntDef(prefix = { "DISMISSAL_SURFACE_" }, value = {
+ DISMISSAL_NOT_DISMISSED, DISMISSAL_OTHER, DISMISSAL_PEEK, DISMISSAL_AOD, DISMISSAL_SHADE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DismissalSurface {}
+
+
+ private @DismissalSurface int mDismissalSurface = DISMISSAL_NOT_DISMISSED;
+
+ /**
+ * Notification has not been dismissed yet.
+ */
+ public static final int DISMISSAL_NOT_DISMISSED = -1;
+ /**
+ * Notification has been dismissed from a {@link NotificationListenerService} or the app
+ * itself.
+ */
+ public static final int DISMISSAL_OTHER = 0;
+ /**
+ * Notification has been dismissed while peeking.
+ */
+ public static final int DISMISSAL_PEEK = 1;
+ /**
+ * Notification has been dismissed from always on display.
+ */
+ public static final int DISMISSAL_AOD = 2;
+ /**
+ * Notification has been dismissed from the notification shade.
+ */
+ public static final int DISMISSAL_SHADE = 3;
+
+ /** @hide */
+ @IntDef(prefix = { "DISMISS_SENTIMENT_" }, value = {
+ DISMISS_SENTIMENT_UNKNOWN, DISMISS_SENTIMENT_NEGATIVE, DISMISS_SENTIMENT_NEUTRAL,
+ DISMISS_SENTIMENT_POSITIVE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DismissalSentiment {}
+
+ /**
+ * No information is available about why this notification was dismissed, or the notification
+ * isn't dismissed yet.
+ */
+ public static final int DISMISS_SENTIMENT_UNKNOWN = -1000;
+ /**
+ * The user indicated while dismissing that they did not like the notification.
+ */
+ public static final int DISMISS_SENTIMENT_NEGATIVE = 0;
+ /**
+ * The user didn't indicate one way or another how they felt about the notification while
+ * dismissing it.
+ */
+ public static final int DISMISS_SENTIMENT_NEUTRAL = 1;
+ /**
+ * The user indicated while dismissing that they did like the notification.
+ */
+ public static final int DISMISS_SENTIMENT_POSITIVE = 2;
+
+
+ private @DismissalSentiment
+ int mDismissalSentiment = DISMISS_SENTIMENT_UNKNOWN;
+
+ public NotificationStats() {
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ protected NotificationStats(Parcel in) {
+ mSeen = in.readByte() != 0;
+ mExpanded = in.readByte() != 0;
+ mDirectReplied = in.readByte() != 0;
+ mSnoozed = in.readByte() != 0;
+ mViewedSettings = in.readByte() != 0;
+ mInteracted = in.readByte() != 0;
+ mDismissalSurface = in.readInt();
+ mDismissalSentiment = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByte((byte) (mSeen ? 1 : 0));
+ dest.writeByte((byte) (mExpanded ? 1 : 0));
+ dest.writeByte((byte) (mDirectReplied ? 1 : 0));
+ dest.writeByte((byte) (mSnoozed ? 1 : 0));
+ dest.writeByte((byte) (mViewedSettings ? 1 : 0));
+ dest.writeByte((byte) (mInteracted ? 1 : 0));
+ dest.writeInt(mDismissalSurface);
+ dest.writeInt(mDismissalSentiment);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Creator<NotificationStats> CREATOR = new Creator<NotificationStats>() {
+ @Override
+ public NotificationStats createFromParcel(Parcel in) {
+ return new NotificationStats(in);
+ }
+
+ @Override
+ public NotificationStats[] newArray(int size) {
+ return new NotificationStats[size];
+ }
+ };
+
+ /**
+ * Returns whether the user has seen this notification at least once.
+ */
+ public boolean hasSeen() {
+ return mSeen;
+ }
+
+ /**
+ * Records that the user as seen this notification at least once.
+ */
+ public void setSeen() {
+ mSeen = true;
+ }
+
+ /**
+ * Returns whether the user has expanded this notification at least once.
+ */
+ public boolean hasExpanded() {
+ return mExpanded;
+ }
+
+ /**
+ * Records that the user has expanded this notification at least once.
+ */
+ public void setExpanded() {
+ mExpanded = true;
+ mInteracted = true;
+ }
+
+ /**
+ * Returns whether the user has replied to a notification that has a
+ * {@link android.app.Notification.Action.Builder#addRemoteInput(RemoteInput) direct reply} at
+ * least once.
+ */
+ public boolean hasDirectReplied() {
+ return mDirectReplied;
+ }
+
+ /**
+ * Records that the user has replied to a notification that has a
+ * {@link android.app.Notification.Action.Builder#addRemoteInput(RemoteInput) direct reply}
+ * at least once.
+ */
+ public void setDirectReplied() {
+ mDirectReplied = true;
+ mInteracted = true;
+ }
+
+ /**
+ * Returns whether the user has snoozed this notification at least once.
+ */
+ public boolean hasSnoozed() {
+ return mSnoozed;
+ }
+
+ /**
+ * Records that the user has snoozed this notification at least once.
+ */
+ public void setSnoozed() {
+ mSnoozed = true;
+ mInteracted = true;
+ }
+
+ /**
+ * Returns whether the user has viewed the in-shade settings for this notification at least
+ * once.
+ */
+ public boolean hasViewedSettings() {
+ return mViewedSettings;
+ }
+
+ /**
+ * Records that the user has viewed the in-shade settings for this notification at least once.
+ */
+ public void setViewedSettings() {
+ mViewedSettings = true;
+ mInteracted = true;
+ }
+
+ /**
+ * Returns whether the user has interacted with this notification beyond having viewed it.
+ */
+ public boolean hasInteracted() {
+ return mInteracted;
+ }
+
+ /**
+ * Returns from which surface the notification was dismissed.
+ */
+ public @DismissalSurface int getDismissalSurface() {
+ return mDismissalSurface;
+ }
+
+ /**
+ * Returns from which surface the notification was dismissed.
+ */
+ public void setDismissalSurface(@DismissalSurface int dismissalSurface) {
+ mDismissalSurface = dismissalSurface;
+ }
+
+ /**
+ * Records whether the user indicated how they felt about a notification before or
+ * during dismissal.
+ */
+ public void setDismissalSentiment(@DismissalSentiment int dismissalSentiment) {
+ mDismissalSentiment = dismissalSentiment;
+ }
+
+ /**
+ * Returns how the user indicated they felt about a notification before or during dismissal.
+ */
+ public @DismissalSentiment int getDismissalSentiment() {
+ return mDismissalSentiment;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ NotificationStats that = (NotificationStats) o;
+
+ if (mSeen != that.mSeen) return false;
+ if (mExpanded != that.mExpanded) return false;
+ if (mDirectReplied != that.mDirectReplied) return false;
+ if (mSnoozed != that.mSnoozed) return false;
+ if (mViewedSettings != that.mViewedSettings) return false;
+ if (mInteracted != that.mInteracted) return false;
+ return mDismissalSurface == that.mDismissalSurface;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (mSeen ? 1 : 0);
+ result = 31 * result + (mExpanded ? 1 : 0);
+ result = 31 * result + (mDirectReplied ? 1 : 0);
+ result = 31 * result + (mSnoozed ? 1 : 0);
+ result = 31 * result + (mViewedSettings ? 1 : 0);
+ result = 31 * result + (mInteracted ? 1 : 0);
+ result = 31 * result + mDismissalSurface;
+ return result;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("NotificationStats{");
+ sb.append("mSeen=").append(mSeen);
+ sb.append(", mExpanded=").append(mExpanded);
+ sb.append(", mDirectReplied=").append(mDirectReplied);
+ sb.append(", mSnoozed=").append(mSnoozed);
+ sb.append(", mViewedSettings=").append(mViewedSettings);
+ sb.append(", mInteracted=").append(mInteracted);
+ sb.append(", mDismissalSurface=").append(mDismissalSurface);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/android/service/notification/NotifyingApp.java b/android/service/notification/NotifyingApp.java
new file mode 100644
index 0000000..a4fc5fd
--- /dev/null
+++ b/android/service/notification/NotifyingApp.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2018 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.notification;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+public final class NotifyingApp implements Parcelable, Comparable<NotifyingApp> {
+
+ private int mUserId;
+ private String mPkg;
+ private long mLastNotified;
+
+ public NotifyingApp() {}
+
+ protected NotifyingApp(Parcel in) {
+ mUserId = in.readInt();
+ mPkg = in.readString();
+ mLastNotified = in.readLong();
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ /**
+ * Sets the userid of the package that sent the notification. Returns self.
+ */
+ public NotifyingApp setUserId(int mUserId) {
+ this.mUserId = mUserId;
+ return this;
+ }
+
+ public String getPackage() {
+ return mPkg;
+ }
+
+ /**
+ * Sets the package that sent the notification. Returns self.
+ */
+ public NotifyingApp setPackage(@NonNull String mPkg) {
+ this.mPkg = mPkg;
+ return this;
+ }
+
+ public long getLastNotified() {
+ return mLastNotified;
+ }
+
+ /**
+ * Sets the time the notification was originally sent. Returns self.
+ */
+ public NotifyingApp setLastNotified(long mLastNotified) {
+ this.mLastNotified = mLastNotified;
+ return this;
+ }
+
+ public static final @NonNull Creator<NotifyingApp> CREATOR = new Creator<NotifyingApp>() {
+ @Override
+ public NotifyingApp createFromParcel(Parcel in) {
+ return new NotifyingApp(in);
+ }
+
+ @Override
+ public NotifyingApp[] newArray(int size) {
+ return new NotifyingApp[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mUserId);
+ dest.writeString(mPkg);
+ dest.writeLong(mLastNotified);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ NotifyingApp that = (NotifyingApp) o;
+ return getUserId() == that.getUserId()
+ && getLastNotified() == that.getLastNotified()
+ && Objects.equals(mPkg, that.mPkg);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getUserId(), mPkg, getLastNotified());
+ }
+
+ /**
+ * Sorts notifying apps from newest last notified date to oldest.
+ */
+ @Override
+ public int compareTo(NotifyingApp o) {
+ if (getLastNotified() == o.getLastNotified()) {
+ if (getUserId() == o.getUserId()) {
+ return getPackage().compareTo(o.getPackage());
+ }
+ return Integer.compare(getUserId(), o.getUserId());
+ }
+
+ return -Long.compare(getLastNotified(), o.getLastNotified());
+ }
+
+ @Override
+ public String toString() {
+ return "NotifyingApp{"
+ + "mUserId=" + mUserId
+ + ", mPkg='" + mPkg + '\''
+ + ", mLastNotified=" + mLastNotified
+ + '}';
+ }
+}
diff --git a/android/service/notification/ScheduleCalendar.java b/android/service/notification/ScheduleCalendar.java
new file mode 100644
index 0000000..6ed966e
--- /dev/null
+++ b/android/service/notification/ScheduleCalendar.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2017 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.notification;
+
+import android.service.notification.ZenModeConfig.ScheduleInfo;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.util.Calendar;
+import java.util.Objects;
+import java.util.TimeZone;
+
+/**
+ * @hide
+ */
+public class ScheduleCalendar {
+ public static final String TAG = "ScheduleCalendar";
+ public static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
+ private final ArraySet<Integer> mDays = new ArraySet<Integer>();
+ private final Calendar mCalendar = Calendar.getInstance();
+
+ private ScheduleInfo mSchedule;
+
+ @Override
+ public String toString() {
+ return "ScheduleCalendar[mDays=" + mDays + ", mSchedule=" + mSchedule + "]";
+ }
+
+ /**
+ * @return true if schedule will exit on alarm, else false
+ */
+ public boolean exitAtAlarm() {
+ return mSchedule.exitAtAlarm;
+ }
+
+ /**
+ * Sets schedule information
+ */
+ public void setSchedule(ScheduleInfo schedule) {
+ if (Objects.equals(mSchedule, schedule)) return;
+ mSchedule = schedule;
+ updateDays();
+ }
+
+ /**
+ * Sets next alarm of the schedule if the saved next alarm has passed or is further
+ * in the future than given nextAlarm
+ * @param now current time in milliseconds
+ * @param nextAlarm time of next alarm in milliseconds
+ */
+ public void maybeSetNextAlarm(long now, long nextAlarm) {
+ if (mSchedule != null && mSchedule.exitAtAlarm) {
+ // alarm canceled
+ if (nextAlarm == 0) {
+ mSchedule.nextAlarm = 0;
+ }
+ // only allow alarms in the future
+ if (nextAlarm > now) {
+ if (mSchedule.nextAlarm == 0 || mSchedule.nextAlarm < now) {
+ mSchedule.nextAlarm = nextAlarm;
+ } else {
+ // store earliest alarm
+ mSchedule.nextAlarm = Math.min(mSchedule.nextAlarm, nextAlarm);
+ }
+ } else if (mSchedule.nextAlarm < now) {
+ if (DEBUG) {
+ Log.d(TAG, "All alarms are in the past " + mSchedule.nextAlarm);
+ }
+ mSchedule.nextAlarm = 0;
+ }
+ }
+ }
+
+ /**
+ * Set calendar time zone to tz
+ * @param tz current time zone
+ */
+ public void setTimeZone(TimeZone tz) {
+ mCalendar.setTimeZone(tz);
+ }
+
+ /**
+ * @param now current time in milliseconds
+ * @return next time this rule changes (starts or ends)
+ */
+ public long getNextChangeTime(long now) {
+ if (mSchedule == null) return 0;
+ final long nextStart = getNextTime(now, mSchedule.startHour, mSchedule.startMinute);
+ final long nextEnd = getNextTime(now, mSchedule.endHour, mSchedule.endMinute);
+ long nextScheduleTime = Math.min(nextStart, nextEnd);
+
+ return nextScheduleTime;
+ }
+
+ private long getNextTime(long now, int hr, int min) {
+ final long time = getTime(now, hr, min);
+ return time <= now ? addDays(time, 1) : time;
+ }
+
+ private long getTime(long millis, int hour, int min) {
+ mCalendar.setTimeInMillis(millis);
+ mCalendar.set(Calendar.HOUR_OF_DAY, hour);
+ mCalendar.set(Calendar.MINUTE, min);
+ mCalendar.set(Calendar.SECOND, 0);
+ mCalendar.set(Calendar.MILLISECOND, 0);
+ return mCalendar.getTimeInMillis();
+ }
+
+ /**
+ * @param time milliseconds since Epoch
+ * @return true if time is within the schedule, else false
+ */
+ public boolean isInSchedule(long time) {
+ if (mSchedule == null || mDays.size() == 0) return false;
+ final long start = getTime(time, mSchedule.startHour, mSchedule.startMinute);
+ long end = getTime(time, mSchedule.endHour, mSchedule.endMinute);
+ if (end <= start) {
+ end = addDays(end, 1);
+ }
+ return isInSchedule(-1, time, start, end) || isInSchedule(0, time, start, end);
+ }
+
+ /**
+ * @param alarm milliseconds since Epoch
+ * @param now milliseconds since Epoch
+ * @return true if alarm and now is within the schedule, else false
+ */
+ public boolean isAlarmInSchedule(long alarm, long now) {
+ if (mSchedule == null || mDays.size() == 0) return false;
+ final long start = getTime(alarm, mSchedule.startHour, mSchedule.startMinute);
+ long end = getTime(alarm, mSchedule.endHour, mSchedule.endMinute);
+ if (end <= start) {
+ end = addDays(end, 1);
+ }
+ return (isInSchedule(-1, alarm, start, end)
+ && isInSchedule(-1, now, start, end))
+ || (isInSchedule(0, alarm, start, end)
+ && isInSchedule(0, now, start, end));
+ }
+
+ /**
+ * @param time milliseconds since Epoch
+ * @return true if should exit at time for next alarm, else false
+ */
+ public boolean shouldExitForAlarm(long time) {
+ if (mSchedule == null) {
+ return false;
+ }
+ return mSchedule.exitAtAlarm
+ && mSchedule.nextAlarm != 0
+ && time >= mSchedule.nextAlarm
+ && isAlarmInSchedule(mSchedule.nextAlarm, time);
+ }
+
+ private boolean isInSchedule(int daysOffset, long time, long start, long end) {
+ final int n = Calendar.SATURDAY;
+ final int day = ((getDayOfWeek(time) - 1) + (daysOffset % n) + n) % n + 1;
+ start = addDays(start, daysOffset);
+ end = addDays(end, daysOffset);
+ return mDays.contains(day) && time >= start && time < end;
+ }
+
+ private int getDayOfWeek(long time) {
+ mCalendar.setTimeInMillis(time);
+ return mCalendar.get(Calendar.DAY_OF_WEEK);
+ }
+
+ private void updateDays() {
+ mDays.clear();
+ if (mSchedule != null && mSchedule.days != null) {
+ for (int i = 0; i < mSchedule.days.length; i++) {
+ mDays.add(mSchedule.days[i]);
+ }
+ }
+ }
+
+ private long addDays(long time, int days) {
+ mCalendar.setTimeInMillis(time);
+ mCalendar.add(Calendar.DATE, days);
+ return mCalendar.getTimeInMillis();
+ }
+}
diff --git a/android/service/notification/SnoozeCriterion.java b/android/service/notification/SnoozeCriterion.java
new file mode 100644
index 0000000..eb624c9
--- /dev/null
+++ b/android/service/notification/SnoozeCriterion.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2016 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.notification;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents an option to be shown to users for snoozing a notification until a given context
+ * instead of for a fixed amount of time.
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class SnoozeCriterion implements Parcelable {
+ private final String mId;
+ private final CharSequence mExplanation;
+ private final CharSequence mConfirmation;
+
+ public SnoozeCriterion(String id, CharSequence explanation, CharSequence confirmation) {
+ mId = id;
+ mExplanation = explanation;
+ mConfirmation = confirmation;
+ }
+
+ protected SnoozeCriterion(Parcel in) {
+ if (in.readByte() != 0) {
+ mId = in.readString();
+ } else {
+ mId = null;
+ }
+ if (in.readByte() != 0) {
+ mExplanation = in.readCharSequence();
+ } else {
+ mExplanation = null;
+ }
+ if (in.readByte() != 0) {
+ mConfirmation = in.readCharSequence();
+ } else {
+ mConfirmation = null;
+ }
+ }
+
+ /**
+ * Returns the id of this criterion.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the user visible explanation of how long a notification will be snoozed if
+ * this criterion is chosen.
+ */
+ public CharSequence getExplanation() {
+ return mExplanation;
+ }
+
+ /**
+ * Returns the user visible confirmation message shown when this criterion is chosen.
+ */
+ public CharSequence getConfirmation() {
+ return mConfirmation;
+ }
+
+ public static final @android.annotation.NonNull Creator<SnoozeCriterion> CREATOR = new Creator<SnoozeCriterion>() {
+ @Override
+ public SnoozeCriterion createFromParcel(Parcel in) {
+ return new SnoozeCriterion(in);
+ }
+
+ @Override
+ public SnoozeCriterion[] newArray(int size) {
+ return new SnoozeCriterion[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mId != null) {
+ dest.writeByte((byte) 1);
+ dest.writeString(mId);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ if (mExplanation != null) {
+ dest.writeByte((byte) 1);
+ dest.writeCharSequence(mExplanation);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ if (mConfirmation != null) {
+ dest.writeByte((byte) 1);
+ dest.writeCharSequence(mConfirmation);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ SnoozeCriterion that = (SnoozeCriterion) o;
+
+ if (mId != null ? !mId.equals(that.mId) : that.mId != null) return false;
+ if (mExplanation != null ? !mExplanation.equals(that.mExplanation)
+ : that.mExplanation != null) {
+ return false;
+ }
+ return mConfirmation != null ? mConfirmation.equals(that.mConfirmation)
+ : that.mConfirmation == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mId != null ? mId.hashCode() : 0;
+ result = 31 * result + (mExplanation != null ? mExplanation.hashCode() : 0);
+ result = 31 * result + (mConfirmation != null ? mConfirmation.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/android/service/notification/StatusBarNotification.java b/android/service/notification/StatusBarNotification.java
new file mode 100644
index 0000000..5c43f8f
--- /dev/null
+++ b/android/service/notification/StatusBarNotification.java
@@ -0,0 +1,535 @@
+/*
+ * Copyright (C) 2008 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.notification;
+
+import static android.app.NotificationChannel.PLACEHOLDER_CONVERSATION_ID;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Person;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.metrics.LogMaker;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.util.ArrayList;
+
+/**
+ * Class encapsulating a Notification. Sent by the NotificationManagerService to clients including
+ * the status bar and any {@link android.service.notification.NotificationListenerService}s.
+ */
+public class StatusBarNotification implements Parcelable {
+ static final int MAX_LOG_TAG_LENGTH = 36;
+
+ @UnsupportedAppUsage
+ private final String pkg;
+ @UnsupportedAppUsage
+ private final int id;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private final String tag;
+ private final String key;
+ private String groupKey;
+ private String overrideGroupKey;
+
+ @UnsupportedAppUsage
+ private final int uid;
+ private final String opPkg;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private final int initialPid;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private final Notification notification;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private final UserHandle user;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private final long postTime;
+ // A small per-notification ID, used for statsd logging.
+ private InstanceId mInstanceId; // Not final, see setInstanceId()
+
+ private Context mContext; // used for inflation & icon expansion
+
+ /** @hide */
+ public StatusBarNotification(String pkg, String opPkg, int id,
+ String tag, int uid, int initialPid, Notification notification, UserHandle user,
+ String overrideGroupKey, long postTime) {
+ if (pkg == null) throw new NullPointerException();
+ if (notification == null) throw new NullPointerException();
+
+ this.pkg = pkg;
+ this.opPkg = opPkg;
+ this.id = id;
+ this.tag = tag;
+ this.uid = uid;
+ this.initialPid = initialPid;
+ this.notification = notification;
+ this.user = user;
+ this.postTime = postTime;
+ this.overrideGroupKey = overrideGroupKey;
+ this.key = key();
+ this.groupKey = groupKey();
+ }
+
+ /**
+ * @deprecated Non-system apps should not need to create StatusBarNotifications.
+ */
+ @Deprecated
+ public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid,
+ int initialPid, int score, Notification notification, UserHandle user,
+ long postTime) {
+ if (pkg == null) throw new NullPointerException();
+ if (notification == null) throw new NullPointerException();
+
+ this.pkg = pkg;
+ this.opPkg = opPkg;
+ this.id = id;
+ this.tag = tag;
+ this.uid = uid;
+ this.initialPid = initialPid;
+ this.notification = notification;
+ this.user = user;
+ this.postTime = postTime;
+ this.key = key();
+ this.groupKey = groupKey();
+ }
+
+ public StatusBarNotification(Parcel in) {
+ this.pkg = in.readString();
+ this.opPkg = in.readString();
+ this.id = in.readInt();
+ if (in.readInt() != 0) {
+ this.tag = in.readString();
+ } else {
+ this.tag = null;
+ }
+ this.uid = in.readInt();
+ this.initialPid = in.readInt();
+ this.notification = new Notification(in);
+ this.user = UserHandle.readFromParcel(in);
+ this.postTime = in.readLong();
+ if (in.readInt() != 0) {
+ this.overrideGroupKey = in.readString();
+ }
+ if (in.readInt() != 0) {
+ this.mInstanceId = InstanceId.CREATOR.createFromParcel(in);
+ }
+ this.key = key();
+ this.groupKey = groupKey();
+ }
+
+ private String key() {
+ String sbnKey = user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid;
+ if (overrideGroupKey != null && getNotification().isGroupSummary()) {
+ sbnKey = sbnKey + "|" + overrideGroupKey;
+ }
+ return sbnKey;
+ }
+
+ private String groupKey() {
+ if (overrideGroupKey != null) {
+ return user.getIdentifier() + "|" + pkg + "|" + "g:" + overrideGroupKey;
+ }
+ final String group = getNotification().getGroup();
+ final String sortKey = getNotification().getSortKey();
+ if (group == null && sortKey == null) {
+ // a group of one
+ return key;
+ }
+ return user.getIdentifier() + "|" + pkg + "|" +
+ (group == null
+ ? "c:" + notification.getChannelId()
+ : "g:" + group);
+ }
+
+ /**
+ * Returns true if this notification is part of a group.
+ */
+ public boolean isGroup() {
+ if (overrideGroupKey != null || isAppGroup()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if application asked that this notification be part of a group.
+ */
+ public boolean isAppGroup() {
+ if (getNotification().getGroup() != null || getNotification().getSortKey() != null) {
+ return true;
+ }
+ return false;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(this.pkg);
+ out.writeString(this.opPkg);
+ out.writeInt(this.id);
+ if (this.tag != null) {
+ out.writeInt(1);
+ out.writeString(this.tag);
+ } else {
+ out.writeInt(0);
+ }
+ out.writeInt(this.uid);
+ out.writeInt(this.initialPid);
+ this.notification.writeToParcel(out, flags);
+ user.writeToParcel(out, flags);
+ out.writeLong(this.postTime);
+ if (this.overrideGroupKey != null) {
+ out.writeInt(1);
+ out.writeString(this.overrideGroupKey);
+ } else {
+ out.writeInt(0);
+ }
+ if (this.mInstanceId != null) {
+ out.writeInt(1);
+ mInstanceId.writeToParcel(out, flags);
+ } else {
+ out.writeInt(0);
+ }
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull
+ Parcelable.Creator<StatusBarNotification> CREATOR =
+ new Parcelable.Creator<StatusBarNotification>() {
+ public StatusBarNotification createFromParcel(Parcel parcel) {
+ return new StatusBarNotification(parcel);
+ }
+
+ public StatusBarNotification[] newArray(int size) {
+ return new StatusBarNotification[size];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ public StatusBarNotification cloneLight() {
+ final Notification no = new Notification();
+ this.notification.cloneInto(no, false); // light copy
+ return cloneShallow(no);
+ }
+
+ @Override
+ public StatusBarNotification clone() {
+ return cloneShallow(this.notification.clone());
+ }
+
+ /**
+ * @param notification Some kind of clone of this.notification.
+ * @return A shallow copy of self, with notification in place of this.notification.
+ */
+ StatusBarNotification cloneShallow(Notification notification) {
+ StatusBarNotification result = new StatusBarNotification(this.pkg, this.opPkg,
+ this.id, this.tag, this.uid, this.initialPid,
+ notification, this.user, this.overrideGroupKey, this.postTime);
+ result.setInstanceId(this.mInstanceId);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "StatusBarNotification(pkg=%s user=%s id=%d tag=%s key=%s: %s)",
+ this.pkg, this.user, this.id, this.tag,
+ this.key, this.notification);
+ }
+
+ /**
+ * Convenience method to check the notification's flags for
+ * {@link Notification#FLAG_ONGOING_EVENT}.
+ */
+ public boolean isOngoing() {
+ return (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0;
+ }
+
+ /**
+ * Convenience method to check the notification's flags for
+ * either {@link Notification#FLAG_ONGOING_EVENT} or
+ * {@link Notification#FLAG_NO_CLEAR}.
+ */
+ public boolean isClearable() {
+ return ((notification.flags & Notification.FLAG_ONGOING_EVENT) == 0)
+ && ((notification.flags & Notification.FLAG_NO_CLEAR) == 0);
+ }
+
+ /**
+ * Returns a userid for whom this notification is intended.
+ *
+ * @deprecated Use {@link #getUser()} instead.
+ */
+ @Deprecated
+ public int getUserId() {
+ return this.user.getIdentifier();
+ }
+
+ /**
+ * Like {@link #getUserId()} but handles special users.
+ * @hide
+ */
+ public int getNormalizedUserId() {
+ int userId = getUserId();
+ if (userId == UserHandle.USER_ALL) {
+ userId = UserHandle.USER_SYSTEM;
+ }
+ return userId;
+ }
+
+ /** The package that the notification belongs to. */
+ public String getPackageName() {
+ return pkg;
+ }
+
+ /** The id supplied to {@link android.app.NotificationManager#notify(int, Notification)}. */
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * The tag supplied to {@link android.app.NotificationManager#notify(int, Notification)},
+ * or null if no tag was specified.
+ */
+ public String getTag() {
+ return tag;
+ }
+
+ /**
+ * The notifying app's ({@link #getPackageName()}'s) uid.
+ */
+ public int getUid() {
+ return uid;
+ }
+
+ /**
+ * The package that posted the notification.
+ * <p> Might be different from {@link #getPackageName()} if the app owning the notification has
+ * a {@link NotificationManager#setNotificationDelegate(String) notification delegate}.
+ */
+ public @NonNull String getOpPkg() {
+ return opPkg;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public int getInitialPid() {
+ return initialPid;
+ }
+
+ /**
+ * The {@link android.app.Notification} supplied to
+ * {@link android.app.NotificationManager#notify(int, Notification)}.
+ */
+ public Notification getNotification() {
+ return notification;
+ }
+
+ /**
+ * The {@link android.os.UserHandle} for whom this notification is intended.
+ */
+ public UserHandle getUser() {
+ return user;
+ }
+
+ /**
+ * The time (in {@link System#currentTimeMillis} time) the notification was posted,
+ * which may be different than {@link android.app.Notification#when}.
+ */
+ public long getPostTime() {
+ return postTime;
+ }
+
+ /**
+ * A unique instance key for this notification record.
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * A key that indicates the group with which this message ranks.
+ */
+ public String getGroupKey() {
+ return groupKey;
+ }
+
+ /**
+ * The ID passed to setGroup(), or the override, or null.
+ *
+ * @hide
+ */
+ public String getGroup() {
+ if (overrideGroupKey != null) {
+ return overrideGroupKey;
+ }
+ return getNotification().getGroup();
+ }
+
+ /**
+ * Sets the override group key.
+ */
+ public void setOverrideGroupKey(String overrideGroupKey) {
+ this.overrideGroupKey = overrideGroupKey;
+ groupKey = groupKey();
+ }
+
+ /**
+ * Returns the override group key.
+ */
+ public String getOverrideGroupKey() {
+ return overrideGroupKey;
+ }
+
+ /**
+ * @hide
+ */
+ public void clearPackageContext() {
+ mContext = null;
+ }
+
+ /**
+ * @hide
+ */
+ public InstanceId getInstanceId() {
+ return mInstanceId;
+ }
+
+ /**
+ * @hide
+ */
+ public void setInstanceId(InstanceId instanceId) {
+ mInstanceId = instanceId;
+ }
+
+ /**
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Context getPackageContext(Context context) {
+ if (mContext == null) {
+ try {
+ ApplicationInfo ai = context.getPackageManager()
+ .getApplicationInfoAsUser(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES,
+ getUserId());
+ mContext = context.createApplicationContext(ai,
+ Context.CONTEXT_RESTRICTED);
+ } catch (PackageManager.NameNotFoundException e) {
+ mContext = null;
+ }
+ }
+ if (mContext == null) {
+ mContext = context;
+ }
+ return mContext;
+ }
+
+ /**
+ * Returns a LogMaker that contains all basic information of the notification.
+ *
+ * @hide
+ */
+ public LogMaker getLogMaker() {
+ LogMaker logMaker = new LogMaker(MetricsEvent.VIEW_UNKNOWN).setPackageName(getPackageName())
+ .addTaggedData(MetricsEvent.NOTIFICATION_ID, getId())
+ .addTaggedData(MetricsEvent.NOTIFICATION_TAG, getTag())
+ .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID, getChannelIdLogTag())
+ .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_ID, getGroupLogTag())
+ .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_SUMMARY,
+ getNotification().isGroupSummary() ? 1 : 0)
+ .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CATEGORY,
+ getNotification().category);
+ if (getNotification().extras != null) {
+ // Log the style used, if present. We only log the hash here, as notification log
+ // events are frequent, while there are few styles (hence low chance of collisions).
+ String template = getNotification().extras.getString(Notification.EXTRA_TEMPLATE);
+ if (template != null && !template.isEmpty()) {
+ logMaker.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_STYLE,
+ template.hashCode());
+ }
+ ArrayList<Person> people = getNotification().extras.getParcelableArrayList(
+ Notification.EXTRA_PEOPLE_LIST);
+ if (people != null && !people.isEmpty()) {
+ logMaker.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_PEOPLE, people.size());
+ }
+ }
+ return logMaker;
+ }
+
+ /**
+ * @hide
+ */
+ public String getShortcutId(Context context) {
+ String conversationId = getNotification().getShortcutId();
+ if (TextUtils.isEmpty(conversationId)
+ && (Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.REQUIRE_SHORTCUTS_FOR_CONVERSATIONS, 0) == 0)
+ && getNotification().getNotificationStyle() == Notification.MessagingStyle.class) {
+ conversationId = getId() + getTag() + PLACEHOLDER_CONVERSATION_ID;
+ }
+ return conversationId;
+ }
+
+ /**
+ * Returns a probably-unique string based on the notification's group name,
+ * with no more than MAX_LOG_TAG_LENGTH characters.
+ * @return String based on group name of notification.
+ * @hide
+ */
+ public String getGroupLogTag() {
+ return shortenTag(getGroup());
+ }
+
+ /**
+ * Returns a probably-unique string based on the notification's channel ID,
+ * with no more than MAX_LOG_TAG_LENGTH characters.
+ * @return String based on channel ID of notification.
+ * @hide
+ */
+ public String getChannelIdLogTag() {
+ if (notification.getChannelId() == null) {
+ return null;
+ }
+ return shortenTag(notification.getChannelId());
+ }
+
+ // Make logTag with max size MAX_LOG_TAG_LENGTH.
+ // For shorter or equal tags, returns the tag.
+ // For longer tags, truncate the tag and append a hash of the full tag to
+ // fill the maximum size.
+ private String shortenTag(String logTag) {
+ if (logTag == null || logTag.length() <= MAX_LOG_TAG_LENGTH) {
+ return logTag;
+ }
+ String hash = Integer.toHexString(logTag.hashCode());
+ return logTag.substring(0, MAX_LOG_TAG_LENGTH - hash.length() - 1) + "-"
+ + hash;
+ }
+}
diff --git a/android/service/notification/ZenModeConfig.java b/android/service/notification/ZenModeConfig.java
new file mode 100644
index 0000000..1f6555c
--- /dev/null
+++ b/android/service/notification/ZenModeConfig.java
@@ -0,0 +1,2134 @@
+/**
+ * Copyright (c) 2014, The Android Open Source Project
+ *
+ * Licensed under the Apache License, 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.notification;
+
+import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE;
+import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT;
+import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_NONE;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS;
+import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
+
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.NotificationManager;
+import android.app.NotificationManager.Policy;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+import android.provider.Settings.Global;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.TimeZone;
+import java.util.UUID;
+
+/**
+ * Persisted configuration for zen mode.
+ *
+ * @hide
+ */
+public class ZenModeConfig implements Parcelable {
+ private static String TAG = "ZenModeConfig";
+
+ public static final int SOURCE_ANYONE = Policy.PRIORITY_SENDERS_ANY;
+ public static final int SOURCE_CONTACT = Policy.PRIORITY_SENDERS_CONTACTS;
+ public static final int SOURCE_STAR = Policy.PRIORITY_SENDERS_STARRED;
+ public static final int MAX_SOURCE = SOURCE_STAR;
+ private static final int DEFAULT_SOURCE = SOURCE_CONTACT;
+ private static final int DEFAULT_CALLS_SOURCE = SOURCE_STAR;
+
+ public static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE";
+ public static final String EVERY_NIGHT_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE";
+ public static final List<String> DEFAULT_RULE_IDS = Arrays.asList(EVERY_NIGHT_DEFAULT_RULE_ID,
+ EVENTS_DEFAULT_RULE_ID);
+
+ public static final int[] ALL_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
+ Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY };
+
+ public static final int[] MINUTE_BUCKETS = generateMinuteBuckets();
+ private static final int SECONDS_MS = 1000;
+ private static final int MINUTES_MS = 60 * SECONDS_MS;
+ private static final int DAY_MINUTES = 24 * 60;
+ private static final int ZERO_VALUE_MS = 10 * SECONDS_MS;
+
+ // Default allow categories set in readXml() from default_zen_mode_config.xml,
+ // fallback/upgrade values:
+ private static final boolean DEFAULT_ALLOW_ALARMS = true;
+ private static final boolean DEFAULT_ALLOW_MEDIA = true;
+ private static final boolean DEFAULT_ALLOW_SYSTEM = false;
+ private static final boolean DEFAULT_ALLOW_CALLS = true;
+ private static final boolean DEFAULT_ALLOW_MESSAGES = false;
+ private static final boolean DEFAULT_ALLOW_REMINDERS = false;
+ private static final boolean DEFAULT_ALLOW_EVENTS = false;
+ private static final boolean DEFAULT_ALLOW_REPEAT_CALLERS = true;
+ private static final boolean DEFAULT_ALLOW_CONV = false;
+ private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_NONE;
+ private static final boolean DEFAULT_CHANNELS_BYPASSING_DND = false;
+ private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS = 0;
+
+ public static final int XML_VERSION = 8;
+ public static final String ZEN_TAG = "zen";
+ private static final String ZEN_ATT_VERSION = "version";
+ private static final String ZEN_ATT_USER = "user";
+ private static final String ALLOW_TAG = "allow";
+ private static final String ALLOW_ATT_ALARMS = "alarms";
+ private static final String ALLOW_ATT_MEDIA = "media";
+ private static final String ALLOW_ATT_SYSTEM = "system";
+ private static final String ALLOW_ATT_CALLS = "calls";
+ private static final String ALLOW_ATT_REPEAT_CALLERS = "repeatCallers";
+ private static final String ALLOW_ATT_MESSAGES = "messages";
+ private static final String ALLOW_ATT_FROM = "from";
+ private static final String ALLOW_ATT_CALLS_FROM = "callsFrom";
+ private static final String ALLOW_ATT_MESSAGES_FROM = "messagesFrom";
+ private static final String ALLOW_ATT_REMINDERS = "reminders";
+ private static final String ALLOW_ATT_EVENTS = "events";
+ private static final String ALLOW_ATT_SCREEN_OFF = "visualScreenOff";
+ private static final String ALLOW_ATT_SCREEN_ON = "visualScreenOn";
+ private static final String ALLOW_ATT_CONV = "convos";
+ private static final String ALLOW_ATT_CONV_FROM = "convosFrom";
+ private static final String DISALLOW_TAG = "disallow";
+ private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects";
+ private static final String STATE_TAG = "state";
+ private static final String STATE_ATT_CHANNELS_BYPASSING_DND = "areChannelsBypassingDnd";
+
+ // zen policy visual effects attributes
+ private static final String SHOW_ATT_FULL_SCREEN_INTENT = "showFullScreenIntent";
+ private static final String SHOW_ATT_LIGHTS = "showLights";
+ private static final String SHOW_ATT_PEEK = "shoePeek";
+ private static final String SHOW_ATT_STATUS_BAR_ICONS = "showStatusBarIcons";
+ private static final String SHOW_ATT_BADGES = "showBadges";
+ private static final String SHOW_ATT_AMBIENT = "showAmbient";
+ private static final String SHOW_ATT_NOTIFICATION_LIST = "showNotificationList";
+
+ private static final String CONDITION_ATT_ID = "id";
+ private static final String CONDITION_ATT_SUMMARY = "summary";
+ private static final String CONDITION_ATT_LINE1 = "line1";
+ private static final String CONDITION_ATT_LINE2 = "line2";
+ private static final String CONDITION_ATT_ICON = "icon";
+ private static final String CONDITION_ATT_STATE = "state";
+ private static final String CONDITION_ATT_FLAGS = "flags";
+
+ private static final String ZEN_POLICY_TAG = "zen_policy";
+
+ private static final String MANUAL_TAG = "manual";
+ private static final String AUTOMATIC_TAG = "automatic";
+
+ private static final String RULE_ATT_ID = "ruleId";
+ private static final String RULE_ATT_ENABLED = "enabled";
+ private static final String RULE_ATT_SNOOZING = "snoozing";
+ private static final String RULE_ATT_NAME = "name";
+ private static final String RULE_ATT_COMPONENT = "component";
+ private static final String RULE_ATT_CONFIG_ACTIVITY = "configActivity";
+ private static final String RULE_ATT_ZEN = "zen";
+ private static final String RULE_ATT_CONDITION_ID = "conditionId";
+ private static final String RULE_ATT_CREATION_TIME = "creationTime";
+ private static final String RULE_ATT_ENABLER = "enabler";
+ private static final String RULE_ATT_MODIFIED = "modified";
+
+ @UnsupportedAppUsage
+ public boolean allowAlarms = DEFAULT_ALLOW_ALARMS;
+ public boolean allowMedia = DEFAULT_ALLOW_MEDIA;
+ public boolean allowSystem = DEFAULT_ALLOW_SYSTEM;
+ public boolean allowCalls = DEFAULT_ALLOW_CALLS;
+ public boolean allowRepeatCallers = DEFAULT_ALLOW_REPEAT_CALLERS;
+ public boolean allowMessages = DEFAULT_ALLOW_MESSAGES;
+ public boolean allowReminders = DEFAULT_ALLOW_REMINDERS;
+ public boolean allowEvents = DEFAULT_ALLOW_EVENTS;
+ public int allowCallsFrom = DEFAULT_CALLS_SOURCE;
+ public int allowMessagesFrom = DEFAULT_SOURCE;
+ public boolean allowConversations = DEFAULT_ALLOW_CONV;
+ public int allowConversationsFrom = DEFAULT_ALLOW_CONV_FROM;
+ public int user = UserHandle.USER_SYSTEM;
+ public int suppressedVisualEffects = DEFAULT_SUPPRESSED_VISUAL_EFFECTS;
+ public boolean areChannelsBypassingDnd = DEFAULT_CHANNELS_BYPASSING_DND;
+ public int version;
+
+ public ZenRule manualRule;
+ @UnsupportedAppUsage
+ public ArrayMap<String, ZenRule> automaticRules = new ArrayMap<>();
+
+ @UnsupportedAppUsage
+ public ZenModeConfig() { }
+
+ public ZenModeConfig(Parcel source) {
+ allowCalls = source.readInt() == 1;
+ allowRepeatCallers = source.readInt() == 1;
+ allowMessages = source.readInt() == 1;
+ allowReminders = source.readInt() == 1;
+ allowEvents = source.readInt() == 1;
+ allowCallsFrom = source.readInt();
+ allowMessagesFrom = source.readInt();
+ user = source.readInt();
+ manualRule = source.readParcelable(null);
+ final int len = source.readInt();
+ if (len > 0) {
+ final String[] ids = new String[len];
+ final ZenRule[] rules = new ZenRule[len];
+ source.readStringArray(ids);
+ source.readTypedArray(rules, ZenRule.CREATOR);
+ for (int i = 0; i < len; i++) {
+ automaticRules.put(ids[i], rules[i]);
+ }
+ }
+ allowAlarms = source.readInt() == 1;
+ allowMedia = source.readInt() == 1;
+ allowSystem = source.readInt() == 1;
+ suppressedVisualEffects = source.readInt();
+ areChannelsBypassingDnd = source.readInt() == 1;
+ allowConversations = source.readBoolean();
+ allowConversationsFrom = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(allowCalls ? 1 : 0);
+ dest.writeInt(allowRepeatCallers ? 1 : 0);
+ dest.writeInt(allowMessages ? 1 : 0);
+ dest.writeInt(allowReminders ? 1 : 0);
+ dest.writeInt(allowEvents ? 1 : 0);
+ dest.writeInt(allowCallsFrom);
+ dest.writeInt(allowMessagesFrom);
+ dest.writeInt(user);
+ dest.writeParcelable(manualRule, 0);
+ if (!automaticRules.isEmpty()) {
+ final int len = automaticRules.size();
+ final String[] ids = new String[len];
+ final ZenRule[] rules = new ZenRule[len];
+ for (int i = 0; i < len; i++) {
+ ids[i] = automaticRules.keyAt(i);
+ rules[i] = automaticRules.valueAt(i);
+ }
+ dest.writeInt(len);
+ dest.writeStringArray(ids);
+ dest.writeTypedArray(rules, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(allowAlarms ? 1 : 0);
+ dest.writeInt(allowMedia ? 1 : 0);
+ dest.writeInt(allowSystem ? 1 : 0);
+ dest.writeInt(suppressedVisualEffects);
+ dest.writeInt(areChannelsBypassingDnd ? 1 : 0);
+ dest.writeBoolean(allowConversations);
+ dest.writeInt(allowConversationsFrom);
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[')
+ .append("user=").append(user)
+ .append(",allowAlarms=").append(allowAlarms)
+ .append(",allowMedia=").append(allowMedia)
+ .append(",allowSystem=").append(allowSystem)
+ .append(",allowReminders=").append(allowReminders)
+ .append(",allowEvents=").append(allowEvents)
+ .append(",allowCalls=").append(allowCalls)
+ .append(",allowRepeatCallers=").append(allowRepeatCallers)
+ .append(",allowMessages=").append(allowMessages)
+ .append(",allowConversations=").append(allowConversations)
+ .append(",allowCallsFrom=").append(sourceToString(allowCallsFrom))
+ .append(",allowMessagesFrom=").append(sourceToString(allowMessagesFrom))
+ .append(",allowConvFrom=").append(ZenPolicy.conversationTypeToString
+ (allowConversationsFrom))
+ .append(",suppressedVisualEffects=").append(suppressedVisualEffects)
+ .append(",areChannelsBypassingDnd=").append(areChannelsBypassingDnd)
+ .append(",\nautomaticRules=").append(rulesToString())
+ .append(",\nmanualRule=").append(manualRule)
+ .append(']').toString();
+ }
+
+ private String rulesToString() {
+ if (automaticRules.isEmpty()) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(automaticRules.size() * 28);
+ buffer.append('{');
+ for (int i = 0; i < automaticRules.size(); i++) {
+ if (i > 0) {
+ buffer.append(",\n");
+ }
+ Object value = automaticRules.valueAt(i);
+ buffer.append(value);
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ public Diff diff(ZenModeConfig to) {
+ final Diff d = new Diff();
+ if (to == null) {
+ return d.addLine("config", "delete");
+ }
+ if (user != to.user) {
+ d.addLine("user", user, to.user);
+ }
+ if (allowAlarms != to.allowAlarms) {
+ d.addLine("allowAlarms", allowAlarms, to.allowAlarms);
+ }
+ if (allowMedia != to.allowMedia) {
+ d.addLine("allowMedia", allowMedia, to.allowMedia);
+ }
+ if (allowSystem != to.allowSystem) {
+ d.addLine("allowSystem", allowSystem, to.allowSystem);
+ }
+ if (allowCalls != to.allowCalls) {
+ d.addLine("allowCalls", allowCalls, to.allowCalls);
+ }
+ if (allowReminders != to.allowReminders) {
+ d.addLine("allowReminders", allowReminders, to.allowReminders);
+ }
+ if (allowEvents != to.allowEvents) {
+ d.addLine("allowEvents", allowEvents, to.allowEvents);
+ }
+ if (allowRepeatCallers != to.allowRepeatCallers) {
+ d.addLine("allowRepeatCallers", allowRepeatCallers, to.allowRepeatCallers);
+ }
+ if (allowMessages != to.allowMessages) {
+ d.addLine("allowMessages", allowMessages, to.allowMessages);
+ }
+ if (allowCallsFrom != to.allowCallsFrom) {
+ d.addLine("allowCallsFrom", allowCallsFrom, to.allowCallsFrom);
+ }
+ if (allowMessagesFrom != to.allowMessagesFrom) {
+ d.addLine("allowMessagesFrom", allowMessagesFrom, to.allowMessagesFrom);
+ }
+ if (suppressedVisualEffects != to.suppressedVisualEffects) {
+ d.addLine("suppressedVisualEffects", suppressedVisualEffects,
+ to.suppressedVisualEffects);
+ }
+ final ArraySet<String> allRules = new ArraySet<>();
+ addKeys(allRules, automaticRules);
+ addKeys(allRules, to.automaticRules);
+ final int N = allRules.size();
+ for (int i = 0; i < N; i++) {
+ final String rule = allRules.valueAt(i);
+ final ZenRule fromRule = automaticRules != null ? automaticRules.get(rule) : null;
+ final ZenRule toRule = to.automaticRules != null ? to.automaticRules.get(rule) : null;
+ ZenRule.appendDiff(d, "automaticRule[" + rule + "]", fromRule, toRule);
+ }
+ ZenRule.appendDiff(d, "manualRule", manualRule, to.manualRule);
+
+ if (areChannelsBypassingDnd != to.areChannelsBypassingDnd) {
+ d.addLine("areChannelsBypassingDnd", areChannelsBypassingDnd,
+ to.areChannelsBypassingDnd);
+ }
+ return d;
+ }
+
+ public static Diff diff(ZenModeConfig from, ZenModeConfig to) {
+ if (from == null) {
+ final Diff d = new Diff();
+ if (to != null) {
+ d.addLine("config", "insert");
+ }
+ return d;
+ }
+ return from.diff(to);
+ }
+
+ private static <T> void addKeys(ArraySet<T> set, ArrayMap<T, ?> map) {
+ if (map != null) {
+ for (int i = 0; i < map.size(); i++) {
+ set.add(map.keyAt(i));
+ }
+ }
+ }
+
+ public boolean isValid() {
+ if (!isValidManualRule(manualRule)) return false;
+ final int N = automaticRules.size();
+ for (int i = 0; i < N; i++) {
+ if (!isValidAutomaticRule(automaticRules.valueAt(i))) return false;
+ }
+ return true;
+ }
+
+ private static boolean isValidManualRule(ZenRule rule) {
+ return rule == null || Global.isValidZenMode(rule.zenMode) && sameCondition(rule);
+ }
+
+ private static boolean isValidAutomaticRule(ZenRule rule) {
+ return rule != null && !TextUtils.isEmpty(rule.name) && Global.isValidZenMode(rule.zenMode)
+ && rule.conditionId != null && sameCondition(rule);
+ }
+
+ private static boolean sameCondition(ZenRule rule) {
+ if (rule == null) return false;
+ if (rule.conditionId == null) {
+ return rule.condition == null;
+ } else {
+ return rule.condition == null || rule.conditionId.equals(rule.condition.id);
+ }
+ }
+
+ private static int[] generateMinuteBuckets() {
+ final int maxHrs = 12;
+ final int[] buckets = new int[maxHrs + 3];
+ buckets[0] = 15;
+ buckets[1] = 30;
+ buckets[2] = 45;
+ for (int i = 1; i <= maxHrs; i++) {
+ buckets[2 + i] = 60 * i;
+ }
+ return buckets;
+ }
+
+ public static String sourceToString(int source) {
+ switch (source) {
+ case SOURCE_ANYONE:
+ return "anyone";
+ case SOURCE_CONTACT:
+ return "contacts";
+ case SOURCE_STAR:
+ return "stars";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ZenModeConfig)) return false;
+ if (o == this) return true;
+ final ZenModeConfig other = (ZenModeConfig) o;
+ return other.allowAlarms == allowAlarms
+ && other.allowMedia == allowMedia
+ && other.allowSystem == allowSystem
+ && other.allowCalls == allowCalls
+ && other.allowRepeatCallers == allowRepeatCallers
+ && other.allowMessages == allowMessages
+ && other.allowCallsFrom == allowCallsFrom
+ && other.allowMessagesFrom == allowMessagesFrom
+ && other.allowReminders == allowReminders
+ && other.allowEvents == allowEvents
+ && other.user == user
+ && Objects.equals(other.automaticRules, automaticRules)
+ && Objects.equals(other.manualRule, manualRule)
+ && other.suppressedVisualEffects == suppressedVisualEffects
+ && other.areChannelsBypassingDnd == areChannelsBypassingDnd
+ && other.allowConversations == allowConversations
+ && other.allowConversationsFrom == allowConversationsFrom;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(allowAlarms, allowMedia, allowSystem, allowCalls,
+ allowRepeatCallers, allowMessages,
+ allowCallsFrom, allowMessagesFrom, allowReminders, allowEvents,
+ user, automaticRules, manualRule,
+ suppressedVisualEffects, areChannelsBypassingDnd, allowConversations,
+ allowConversationsFrom);
+ }
+
+ private static String toDayList(int[] days) {
+ if (days == null || days.length == 0) return "";
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < days.length; i++) {
+ if (i > 0) sb.append('.');
+ sb.append(days[i]);
+ }
+ return sb.toString();
+ }
+
+ private static int[] tryParseDayList(String dayList, String sep) {
+ if (dayList == null) return null;
+ final String[] tokens = dayList.split(sep);
+ if (tokens.length == 0) return null;
+ final int[] rt = new int[tokens.length];
+ for (int i = 0; i < tokens.length; i++) {
+ final int day = tryParseInt(tokens[i], -1);
+ if (day == -1) return null;
+ rt[i] = day;
+ }
+ return rt;
+ }
+
+ private static int tryParseInt(String value, int defValue) {
+ if (TextUtils.isEmpty(value)) return defValue;
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ }
+
+ private static long tryParseLong(String value, long defValue) {
+ if (TextUtils.isEmpty(value)) return defValue;
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ }
+
+ private static Long tryParseLong(String value, Long defValue) {
+ if (TextUtils.isEmpty(value)) return defValue;
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ }
+
+ public static ZenModeConfig readXml(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ int type = parser.getEventType();
+ if (type != XmlPullParser.START_TAG) return null;
+ String tag = parser.getName();
+ if (!ZEN_TAG.equals(tag)) return null;
+ final ZenModeConfig rt = new ZenModeConfig();
+ rt.version = safeInt(parser, ZEN_ATT_VERSION, XML_VERSION);
+ rt.user = safeInt(parser, ZEN_ATT_USER, rt.user);
+ boolean readSuppressedEffects = false;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ tag = parser.getName();
+ if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) {
+ return rt;
+ }
+ if (type == XmlPullParser.START_TAG) {
+ if (ALLOW_TAG.equals(tag)) {
+ rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS,
+ DEFAULT_ALLOW_CALLS);
+ rt.allowRepeatCallers = safeBoolean(parser, ALLOW_ATT_REPEAT_CALLERS,
+ DEFAULT_ALLOW_REPEAT_CALLERS);
+ rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES,
+ DEFAULT_ALLOW_MESSAGES);
+ rt.allowReminders = safeBoolean(parser, ALLOW_ATT_REMINDERS,
+ DEFAULT_ALLOW_REMINDERS);
+ rt.allowConversations = safeBoolean(parser, ALLOW_ATT_CONV, DEFAULT_ALLOW_CONV);
+ rt.allowEvents = safeBoolean(parser, ALLOW_ATT_EVENTS, DEFAULT_ALLOW_EVENTS);
+ final int from = safeInt(parser, ALLOW_ATT_FROM, -1);
+ final int callsFrom = safeInt(parser, ALLOW_ATT_CALLS_FROM, -1);
+ final int messagesFrom = safeInt(parser, ALLOW_ATT_MESSAGES_FROM, -1);
+ rt.allowConversationsFrom = safeInt(parser, ALLOW_ATT_CONV_FROM,
+ DEFAULT_ALLOW_CONV_FROM);
+ if (isValidSource(callsFrom) && isValidSource(messagesFrom)) {
+ rt.allowCallsFrom = callsFrom;
+ rt.allowMessagesFrom = messagesFrom;
+ } else if (isValidSource(from)) {
+ Slog.i(TAG, "Migrating existing shared 'from': " + sourceToString(from));
+ rt.allowCallsFrom = from;
+ rt.allowMessagesFrom = from;
+ } else {
+ rt.allowCallsFrom = DEFAULT_CALLS_SOURCE;
+ rt.allowMessagesFrom = DEFAULT_SOURCE;
+ }
+ rt.allowAlarms = safeBoolean(parser, ALLOW_ATT_ALARMS, DEFAULT_ALLOW_ALARMS);
+ rt.allowMedia = safeBoolean(parser, ALLOW_ATT_MEDIA,
+ DEFAULT_ALLOW_MEDIA);
+ rt.allowSystem = safeBoolean(parser, ALLOW_ATT_SYSTEM, DEFAULT_ALLOW_SYSTEM);
+
+ // migrate old suppressed visual effects fields, if they still exist in the xml
+ Boolean allowWhenScreenOff = unsafeBoolean(parser, ALLOW_ATT_SCREEN_OFF);
+ if (allowWhenScreenOff != null) {
+ readSuppressedEffects = true;
+ if (!allowWhenScreenOff) {
+ rt.suppressedVisualEffects |= SUPPRESSED_EFFECT_LIGHTS
+ | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT;
+ }
+ }
+ Boolean allowWhenScreenOn = unsafeBoolean(parser, ALLOW_ATT_SCREEN_ON);
+ if (allowWhenScreenOn != null) {
+ readSuppressedEffects = true;
+ if (!allowWhenScreenOn) {
+ rt.suppressedVisualEffects |= SUPPRESSED_EFFECT_PEEK;
+ }
+ }
+ if (readSuppressedEffects) {
+ Slog.d(TAG, "Migrated visual effects to " + rt.suppressedVisualEffects);
+ }
+ } else if (DISALLOW_TAG.equals(tag) && !readSuppressedEffects) {
+ // only read from suppressed visual effects field if we haven't just migrated
+ // the values from allowOn/allowOff, lest we wipe out those settings
+ rt.suppressedVisualEffects = safeInt(parser, DISALLOW_ATT_VISUAL_EFFECTS,
+ DEFAULT_SUPPRESSED_VISUAL_EFFECTS);
+ } else if (MANUAL_TAG.equals(tag)) {
+ rt.manualRule = readRuleXml(parser);
+ } else if (AUTOMATIC_TAG.equals(tag)) {
+ final String id = parser.getAttributeValue(null, RULE_ATT_ID);
+ final ZenRule automaticRule = readRuleXml(parser);
+ if (id != null && automaticRule != null) {
+ automaticRule.id = id;
+ rt.automaticRules.put(id, automaticRule);
+ }
+ } else if (STATE_TAG.equals(tag)) {
+ rt.areChannelsBypassingDnd = safeBoolean(parser,
+ STATE_ATT_CHANNELS_BYPASSING_DND, DEFAULT_CHANNELS_BYPASSING_DND);
+ }
+ }
+ }
+ throw new IllegalStateException("Failed to reach END_DOCUMENT");
+ }
+
+ /**
+ * Writes XML of current ZenModeConfig
+ * @param out serializer
+ * @param version uses XML_VERSION if version is null
+ * @throws IOException
+ */
+ public void writeXml(XmlSerializer out, Integer version) throws IOException {
+ out.startTag(null, ZEN_TAG);
+ out.attribute(null, ZEN_ATT_VERSION, version == null
+ ? Integer.toString(XML_VERSION) : Integer.toString(version));
+ out.attribute(null, ZEN_ATT_USER, Integer.toString(user));
+ out.startTag(null, ALLOW_TAG);
+ out.attribute(null, ALLOW_ATT_CALLS, Boolean.toString(allowCalls));
+ out.attribute(null, ALLOW_ATT_REPEAT_CALLERS, Boolean.toString(allowRepeatCallers));
+ out.attribute(null, ALLOW_ATT_MESSAGES, Boolean.toString(allowMessages));
+ out.attribute(null, ALLOW_ATT_REMINDERS, Boolean.toString(allowReminders));
+ out.attribute(null, ALLOW_ATT_EVENTS, Boolean.toString(allowEvents));
+ out.attribute(null, ALLOW_ATT_CALLS_FROM, Integer.toString(allowCallsFrom));
+ out.attribute(null, ALLOW_ATT_MESSAGES_FROM, Integer.toString(allowMessagesFrom));
+ out.attribute(null, ALLOW_ATT_ALARMS, Boolean.toString(allowAlarms));
+ out.attribute(null, ALLOW_ATT_MEDIA, Boolean.toString(allowMedia));
+ out.attribute(null, ALLOW_ATT_SYSTEM, Boolean.toString(allowSystem));
+ out.attribute(null, ALLOW_ATT_CONV, Boolean.toString(allowConversations));
+ out.attribute(null, ALLOW_ATT_CONV_FROM, Integer.toString(allowConversationsFrom));
+ out.endTag(null, ALLOW_TAG);
+
+ out.startTag(null, DISALLOW_TAG);
+ out.attribute(null, DISALLOW_ATT_VISUAL_EFFECTS, Integer.toString(suppressedVisualEffects));
+ out.endTag(null, DISALLOW_TAG);
+
+ if (manualRule != null) {
+ out.startTag(null, MANUAL_TAG);
+ writeRuleXml(manualRule, out);
+ out.endTag(null, MANUAL_TAG);
+ }
+ final int N = automaticRules.size();
+ for (int i = 0; i < N; i++) {
+ final String id = automaticRules.keyAt(i);
+ final ZenRule automaticRule = automaticRules.valueAt(i);
+ out.startTag(null, AUTOMATIC_TAG);
+ out.attribute(null, RULE_ATT_ID, id);
+ writeRuleXml(automaticRule, out);
+ out.endTag(null, AUTOMATIC_TAG);
+ }
+
+ out.startTag(null, STATE_TAG);
+ out.attribute(null, STATE_ATT_CHANNELS_BYPASSING_DND,
+ Boolean.toString(areChannelsBypassingDnd));
+ out.endTag(null, STATE_TAG);
+
+ out.endTag(null, ZEN_TAG);
+ }
+
+ public static ZenRule readRuleXml(XmlPullParser parser) {
+ final ZenRule rt = new ZenRule();
+ rt.enabled = safeBoolean(parser, RULE_ATT_ENABLED, true);
+ rt.name = parser.getAttributeValue(null, RULE_ATT_NAME);
+ final String zen = parser.getAttributeValue(null, RULE_ATT_ZEN);
+ rt.zenMode = tryParseZenMode(zen, -1);
+ if (rt.zenMode == -1) {
+ Slog.w(TAG, "Bad zen mode in rule xml:" + zen);
+ return null;
+ }
+ rt.conditionId = safeUri(parser, RULE_ATT_CONDITION_ID);
+ rt.component = safeComponentName(parser, RULE_ATT_COMPONENT);
+ rt.configurationActivity = safeComponentName(parser, RULE_ATT_CONFIG_ACTIVITY);
+ rt.pkg = (rt.component != null)
+ ? rt.component.getPackageName()
+ : (rt.configurationActivity != null)
+ ? rt.configurationActivity.getPackageName()
+ : null;
+ rt.creationTime = safeLong(parser, RULE_ATT_CREATION_TIME, 0);
+ rt.enabler = parser.getAttributeValue(null, RULE_ATT_ENABLER);
+ rt.condition = readConditionXml(parser);
+
+ // all default rules and user created rules updated to zenMode important interruptions
+ if (rt.zenMode != Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+ && Condition.isValidId(rt.conditionId, SYSTEM_AUTHORITY)) {
+ Slog.i(TAG, "Updating zenMode of automatic rule " + rt.name);
+ rt.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ }
+ rt.modified = safeBoolean(parser, RULE_ATT_MODIFIED, false);
+ rt.zenPolicy = readZenPolicyXml(parser);
+ return rt;
+ }
+
+ public static void writeRuleXml(ZenRule rule, XmlSerializer out) throws IOException {
+ out.attribute(null, RULE_ATT_ENABLED, Boolean.toString(rule.enabled));
+ if (rule.name != null) {
+ out.attribute(null, RULE_ATT_NAME, rule.name);
+ }
+ out.attribute(null, RULE_ATT_ZEN, Integer.toString(rule.zenMode));
+ if (rule.component != null) {
+ out.attribute(null, RULE_ATT_COMPONENT, rule.component.flattenToString());
+ }
+ if (rule.configurationActivity != null) {
+ out.attribute(null, RULE_ATT_CONFIG_ACTIVITY,
+ rule.configurationActivity.flattenToString());
+ }
+ if (rule.conditionId != null) {
+ out.attribute(null, RULE_ATT_CONDITION_ID, rule.conditionId.toString());
+ }
+ out.attribute(null, RULE_ATT_CREATION_TIME, Long.toString(rule.creationTime));
+ if (rule.enabler != null) {
+ out.attribute(null, RULE_ATT_ENABLER, rule.enabler);
+ }
+ if (rule.condition != null) {
+ writeConditionXml(rule.condition, out);
+ }
+ if (rule.zenPolicy != null) {
+ writeZenPolicyXml(rule.zenPolicy, out);
+ }
+ out.attribute(null, RULE_ATT_MODIFIED, Boolean.toString(rule.modified));
+ }
+
+ public static Condition readConditionXml(XmlPullParser parser) {
+ final Uri id = safeUri(parser, CONDITION_ATT_ID);
+ if (id == null) return null;
+ final String summary = parser.getAttributeValue(null, CONDITION_ATT_SUMMARY);
+ final String line1 = parser.getAttributeValue(null, CONDITION_ATT_LINE1);
+ final String line2 = parser.getAttributeValue(null, CONDITION_ATT_LINE2);
+ final int icon = safeInt(parser, CONDITION_ATT_ICON, -1);
+ final int state = safeInt(parser, CONDITION_ATT_STATE, -1);
+ final int flags = safeInt(parser, CONDITION_ATT_FLAGS, -1);
+ try {
+ return new Condition(id, summary, line1, line2, icon, state, flags);
+ } catch (IllegalArgumentException e) {
+ Slog.w(TAG, "Unable to read condition xml", e);
+ return null;
+ }
+ }
+
+ public static void writeConditionXml(Condition c, XmlSerializer out) throws IOException {
+ out.attribute(null, CONDITION_ATT_ID, c.id.toString());
+ out.attribute(null, CONDITION_ATT_SUMMARY, c.summary);
+ out.attribute(null, CONDITION_ATT_LINE1, c.line1);
+ out.attribute(null, CONDITION_ATT_LINE2, c.line2);
+ out.attribute(null, CONDITION_ATT_ICON, Integer.toString(c.icon));
+ out.attribute(null, CONDITION_ATT_STATE, Integer.toString(c.state));
+ out.attribute(null, CONDITION_ATT_FLAGS, Integer.toString(c.flags));
+ }
+
+ /**
+ * Read the zen policy from xml
+ * Returns null if no zen policy exists
+ */
+ public static ZenPolicy readZenPolicyXml(XmlPullParser parser) {
+ boolean policySet = false;
+
+ ZenPolicy.Builder builder = new ZenPolicy.Builder();
+ final int calls = safeInt(parser, ALLOW_ATT_CALLS_FROM, ZenPolicy.PEOPLE_TYPE_UNSET);
+ final int messages = safeInt(parser, ALLOW_ATT_MESSAGES_FROM, ZenPolicy.PEOPLE_TYPE_UNSET);
+ final int repeatCallers = safeInt(parser, ALLOW_ATT_REPEAT_CALLERS, ZenPolicy.STATE_UNSET);
+ final int alarms = safeInt(parser, ALLOW_ATT_ALARMS, ZenPolicy.STATE_UNSET);
+ final int media = safeInt(parser, ALLOW_ATT_MEDIA, ZenPolicy.STATE_UNSET);
+ final int system = safeInt(parser, ALLOW_ATT_SYSTEM, ZenPolicy.STATE_UNSET);
+ final int events = safeInt(parser, ALLOW_ATT_EVENTS, ZenPolicy.STATE_UNSET);
+ final int reminders = safeInt(parser, ALLOW_ATT_REMINDERS, ZenPolicy.STATE_UNSET);
+
+ if (calls != ZenPolicy.PEOPLE_TYPE_UNSET) {
+ builder.allowCalls(calls);
+ policySet = true;
+ }
+ if (messages != ZenPolicy.PEOPLE_TYPE_UNSET) {
+ builder.allowMessages(messages);
+ policySet = true;
+ }
+ if (repeatCallers != ZenPolicy.STATE_UNSET) {
+ builder.allowRepeatCallers(repeatCallers == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (alarms != ZenPolicy.STATE_UNSET) {
+ builder.allowAlarms(alarms == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (media != ZenPolicy.STATE_UNSET) {
+ builder.allowMedia(media == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (system != ZenPolicy.STATE_UNSET) {
+ builder.allowSystem(system == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (events != ZenPolicy.STATE_UNSET) {
+ builder.allowEvents(events == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (reminders != ZenPolicy.STATE_UNSET) {
+ builder.allowReminders(reminders == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+
+ final int fullScreenIntent = safeInt(parser, SHOW_ATT_FULL_SCREEN_INTENT,
+ ZenPolicy.STATE_UNSET);
+ final int lights = safeInt(parser, SHOW_ATT_LIGHTS, ZenPolicy.STATE_UNSET);
+ final int peek = safeInt(parser, SHOW_ATT_PEEK, ZenPolicy.STATE_UNSET);
+ final int statusBar = safeInt(parser, SHOW_ATT_STATUS_BAR_ICONS, ZenPolicy.STATE_UNSET);
+ final int badges = safeInt(parser, SHOW_ATT_BADGES, ZenPolicy.STATE_UNSET);
+ final int ambient = safeInt(parser, SHOW_ATT_AMBIENT, ZenPolicy.STATE_UNSET);
+ final int notificationList = safeInt(parser, SHOW_ATT_NOTIFICATION_LIST,
+ ZenPolicy.STATE_UNSET);
+
+ if (fullScreenIntent != ZenPolicy.STATE_UNSET) {
+ builder.showFullScreenIntent(fullScreenIntent == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (lights != ZenPolicy.STATE_UNSET) {
+ builder.showLights(lights == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (peek != ZenPolicy.STATE_UNSET) {
+ builder.showPeeking(peek == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (statusBar != ZenPolicy.STATE_UNSET) {
+ builder.showStatusBarIcons(statusBar == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (badges != ZenPolicy.STATE_UNSET) {
+ builder.showBadges(badges == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (ambient != ZenPolicy.STATE_UNSET) {
+ builder.showInAmbientDisplay(ambient == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+ if (notificationList != ZenPolicy.STATE_UNSET) {
+ builder.showInNotificationList(notificationList == ZenPolicy.STATE_ALLOW);
+ policySet = true;
+ }
+
+ if (policySet) {
+ return builder.build();
+ }
+ return null;
+ }
+
+ /**
+ * Writes zen policy to xml
+ */
+ public static void writeZenPolicyXml(ZenPolicy policy, XmlSerializer out)
+ throws IOException {
+ writeZenPolicyState(ALLOW_ATT_CALLS_FROM, policy.getPriorityCallSenders(), out);
+ writeZenPolicyState(ALLOW_ATT_MESSAGES_FROM, policy.getPriorityMessageSenders(), out);
+ writeZenPolicyState(ALLOW_ATT_REPEAT_CALLERS, policy.getPriorityCategoryRepeatCallers(),
+ out);
+ writeZenPolicyState(ALLOW_ATT_ALARMS, policy.getPriorityCategoryAlarms(), out);
+ writeZenPolicyState(ALLOW_ATT_MEDIA, policy.getPriorityCategoryMedia(), out);
+ writeZenPolicyState(ALLOW_ATT_SYSTEM, policy.getPriorityCategorySystem(), out);
+ writeZenPolicyState(ALLOW_ATT_REMINDERS, policy.getPriorityCategoryReminders(), out);
+ writeZenPolicyState(ALLOW_ATT_EVENTS, policy.getPriorityCategoryEvents(), out);
+
+ writeZenPolicyState(SHOW_ATT_FULL_SCREEN_INTENT, policy.getVisualEffectFullScreenIntent(),
+ out);
+ writeZenPolicyState(SHOW_ATT_LIGHTS, policy.getVisualEffectLights(), out);
+ writeZenPolicyState(SHOW_ATT_PEEK, policy.getVisualEffectPeek(), out);
+ writeZenPolicyState(SHOW_ATT_STATUS_BAR_ICONS, policy.getVisualEffectStatusBar(), out);
+ writeZenPolicyState(SHOW_ATT_BADGES, policy.getVisualEffectBadge(), out);
+ writeZenPolicyState(SHOW_ATT_AMBIENT, policy.getVisualEffectAmbient(), out);
+ writeZenPolicyState(SHOW_ATT_NOTIFICATION_LIST, policy.getVisualEffectNotificationList(),
+ out);
+ }
+
+ private static void writeZenPolicyState(String attr, int val, XmlSerializer out)
+ throws IOException {
+ if (Objects.equals(attr, ALLOW_ATT_CALLS_FROM)
+ || Objects.equals(attr, ALLOW_ATT_MESSAGES_FROM)) {
+ if (val != ZenPolicy.PEOPLE_TYPE_UNSET) {
+ out.attribute(null, attr, Integer.toString(val));
+ }
+ } else {
+ if (val != ZenPolicy.STATE_UNSET) {
+ out.attribute(null, attr, Integer.toString(val));
+ }
+ }
+ }
+
+ public static boolean isValidHour(int val) {
+ return val >= 0 && val < 24;
+ }
+
+ public static boolean isValidMinute(int val) {
+ return val >= 0 && val < 60;
+ }
+
+ private static boolean isValidSource(int source) {
+ return source >= SOURCE_ANYONE && source <= MAX_SOURCE;
+ }
+
+ private static Boolean unsafeBoolean(XmlPullParser parser, String att) {
+ final String val = parser.getAttributeValue(null, att);
+ if (TextUtils.isEmpty(val)) return null;
+ return Boolean.parseBoolean(val);
+ }
+
+ private static boolean safeBoolean(XmlPullParser parser, String att, boolean defValue) {
+ final String val = parser.getAttributeValue(null, att);
+ return safeBoolean(val, defValue);
+ }
+
+ private static boolean safeBoolean(String val, boolean defValue) {
+ if (TextUtils.isEmpty(val)) return defValue;
+ return Boolean.parseBoolean(val);
+ }
+
+ private static int safeInt(XmlPullParser parser, String att, int defValue) {
+ final String val = parser.getAttributeValue(null, att);
+ return tryParseInt(val, defValue);
+ }
+
+ private static ComponentName safeComponentName(XmlPullParser parser, String att) {
+ final String val = parser.getAttributeValue(null, att);
+ if (TextUtils.isEmpty(val)) return null;
+ return ComponentName.unflattenFromString(val);
+ }
+
+ private static Uri safeUri(XmlPullParser parser, String att) {
+ final String val = parser.getAttributeValue(null, att);
+ if (TextUtils.isEmpty(val)) return null;
+ return Uri.parse(val);
+ }
+
+ private static long safeLong(XmlPullParser parser, String att, long defValue) {
+ final String val = parser.getAttributeValue(null, att);
+ return tryParseLong(val, defValue);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public ZenModeConfig copy() {
+ final Parcel parcel = Parcel.obtain();
+ try {
+ writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return new ZenModeConfig(parcel);
+ } finally {
+ parcel.recycle();
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ZenModeConfig> CREATOR
+ = new Parcelable.Creator<ZenModeConfig>() {
+ @Override
+ public ZenModeConfig createFromParcel(Parcel source) {
+ return new ZenModeConfig(source);
+ }
+
+ @Override
+ public ZenModeConfig[] newArray(int size) {
+ return new ZenModeConfig[size];
+ }
+ };
+
+ /**
+ * Converts a zenPolicy to a notificationPolicy using this ZenModeConfig's values as its
+ * defaults for all unset values in zenPolicy
+ */
+ public Policy toNotificationPolicy(ZenPolicy zenPolicy) {
+ NotificationManager.Policy defaultPolicy = toNotificationPolicy();
+ int priorityCategories = 0;
+ int suppressedVisualEffects = 0;
+ int callSenders = defaultPolicy.priorityCallSenders;
+ int messageSenders = defaultPolicy.priorityMessageSenders;
+ int conversationSenders = defaultPolicy.priorityConversationSenders;
+
+ if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_REMINDERS,
+ isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_REMINDERS, defaultPolicy))) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_REMINDERS;
+ }
+
+ if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_EVENTS,
+ isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_EVENTS, defaultPolicy))) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_EVENTS;
+ }
+
+ if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_MESSAGES,
+ isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_MESSAGES, defaultPolicy))) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_MESSAGES;
+ messageSenders = getNotificationPolicySenders(zenPolicy.getPriorityMessageSenders(),
+ messageSenders);
+ }
+
+ if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_CONVERSATIONS,
+ isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_CONVERSATIONS, defaultPolicy))) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
+ conversationSenders = getNotificationPolicySenders(
+ zenPolicy.getPriorityConversationSenders(),
+ conversationSenders);
+ }
+
+ if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_CALLS,
+ isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_CALLS, defaultPolicy))) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_CALLS;
+ callSenders = getNotificationPolicySenders(zenPolicy.getPriorityCallSenders(),
+ callSenders);
+ }
+
+ if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS,
+ isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_REPEAT_CALLERS,
+ defaultPolicy))) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_REPEAT_CALLERS;
+ }
+
+ if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_ALARMS,
+ isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_ALARMS, defaultPolicy))) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_ALARMS;
+ }
+
+ if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_MEDIA,
+ isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_MEDIA, defaultPolicy))) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_MEDIA;
+ }
+
+ if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_SYSTEM,
+ isPriorityCategoryEnabled(Policy.PRIORITY_CATEGORY_SYSTEM, defaultPolicy))) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_SYSTEM;
+ }
+
+ boolean suppressFullScreenIntent = !zenPolicy.isVisualEffectAllowed(
+ ZenPolicy.VISUAL_EFFECT_FULL_SCREEN_INTENT,
+ isVisualEffectAllowed(Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT,
+ defaultPolicy));
+
+ boolean suppressLights = !zenPolicy.isVisualEffectAllowed(
+ ZenPolicy.VISUAL_EFFECT_LIGHTS,
+ isVisualEffectAllowed(Policy.SUPPRESSED_EFFECT_LIGHTS,
+ defaultPolicy));
+
+ boolean suppressAmbient = !zenPolicy.isVisualEffectAllowed(
+ ZenPolicy.VISUAL_EFFECT_AMBIENT,
+ isVisualEffectAllowed(Policy.SUPPRESSED_EFFECT_AMBIENT,
+ defaultPolicy));
+
+ if (suppressFullScreenIntent && suppressLights && suppressAmbient) {
+ suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
+ }
+
+ if (suppressFullScreenIntent) {
+ suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT;
+ }
+
+ if (suppressLights) {
+ suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_LIGHTS;
+ }
+
+ if (!zenPolicy.isVisualEffectAllowed(ZenPolicy.VISUAL_EFFECT_PEEK,
+ isVisualEffectAllowed(Policy.SUPPRESSED_EFFECT_PEEK,
+ defaultPolicy))) {
+ suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_PEEK;
+ suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_SCREEN_ON;
+ }
+
+ if (!zenPolicy.isVisualEffectAllowed(ZenPolicy.VISUAL_EFFECT_STATUS_BAR,
+ isVisualEffectAllowed(Policy.SUPPRESSED_EFFECT_STATUS_BAR,
+ defaultPolicy))) {
+ suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_STATUS_BAR;
+ }
+
+ if (!zenPolicy.isVisualEffectAllowed(ZenPolicy.VISUAL_EFFECT_BADGE,
+ isVisualEffectAllowed(Policy.SUPPRESSED_EFFECT_BADGE,
+ defaultPolicy))) {
+ suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_BADGE;
+ }
+
+ if (suppressAmbient) {
+ suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_AMBIENT;
+ }
+
+ if (!zenPolicy.isVisualEffectAllowed(ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST,
+ isVisualEffectAllowed(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST,
+ defaultPolicy))) {
+ suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
+ }
+
+ return new NotificationManager.Policy(priorityCategories, callSenders,
+ messageSenders, suppressedVisualEffects, defaultPolicy.state, conversationSenders);
+ }
+
+ private boolean isPriorityCategoryEnabled(int categoryType, Policy policy) {
+ return (policy.priorityCategories & categoryType) != 0;
+ }
+
+ private boolean isVisualEffectAllowed(int visualEffect, Policy policy) {
+ return (policy.suppressedVisualEffects & visualEffect) == 0;
+ }
+
+ private int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders,
+ int defaultPolicySender) {
+ switch (senders) {
+ case ZenPolicy.PEOPLE_TYPE_ANYONE:
+ return Policy.PRIORITY_SENDERS_ANY;
+ case ZenPolicy.PEOPLE_TYPE_CONTACTS:
+ return Policy.PRIORITY_SENDERS_CONTACTS;
+ case ZenPolicy.PEOPLE_TYPE_STARRED:
+ return Policy.PRIORITY_SENDERS_STARRED;
+ default:
+ return defaultPolicySender;
+ }
+ }
+
+
+ /**
+ * Maps NotificationManager.Policy senders type to ZenPolicy.PeopleType
+ */
+ public static @ZenPolicy.PeopleType int getZenPolicySenders(int senders) {
+ switch (senders) {
+ case Policy.PRIORITY_SENDERS_ANY:
+ return ZenPolicy.PEOPLE_TYPE_ANYONE;
+ case Policy.PRIORITY_SENDERS_CONTACTS:
+ return ZenPolicy.PEOPLE_TYPE_CONTACTS;
+ case Policy.PRIORITY_SENDERS_STARRED:
+ default:
+ return ZenPolicy.PEOPLE_TYPE_STARRED;
+ }
+ }
+
+ public Policy toNotificationPolicy() {
+ int priorityCategories = 0;
+ int priorityCallSenders = Policy.PRIORITY_SENDERS_CONTACTS;
+ int priorityMessageSenders = Policy.PRIORITY_SENDERS_CONTACTS;
+ int priorityConversationSenders = Policy.CONVERSATION_SENDERS_IMPORTANT;
+ if (allowConversations) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
+ }
+ if (allowCalls) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_CALLS;
+ }
+ if (allowMessages) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_MESSAGES;
+ }
+ if (allowEvents) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_EVENTS;
+ }
+ if (allowReminders) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_REMINDERS;
+ }
+ if (allowRepeatCallers) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_REPEAT_CALLERS;
+ }
+ if (allowAlarms) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_ALARMS;
+ }
+ if (allowMedia) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_MEDIA;
+ }
+ if (allowSystem) {
+ priorityCategories |= Policy.PRIORITY_CATEGORY_SYSTEM;
+ }
+ priorityCallSenders = sourceToPrioritySenders(allowCallsFrom, priorityCallSenders);
+ priorityMessageSenders = sourceToPrioritySenders(allowMessagesFrom, priorityMessageSenders);
+ priorityConversationSenders = allowConversationsFrom;
+
+ return new Policy(priorityCategories, priorityCallSenders, priorityMessageSenders,
+ suppressedVisualEffects, areChannelsBypassingDnd
+ ? Policy.STATE_CHANNELS_BYPASSING_DND : 0,
+ priorityConversationSenders);
+ }
+
+ /**
+ * Creates scheduleCalendar from a condition id
+ * @param conditionId
+ * @return ScheduleCalendar with info populated with conditionId
+ */
+ public static ScheduleCalendar toScheduleCalendar(Uri conditionId) {
+ final ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId(conditionId);
+ if (schedule == null || schedule.days == null || schedule.days.length == 0) return null;
+ final ScheduleCalendar sc = new ScheduleCalendar();
+ sc.setSchedule(schedule);
+ sc.setTimeZone(TimeZone.getDefault());
+ return sc;
+ }
+
+ private static int sourceToPrioritySenders(int source, int def) {
+ switch (source) {
+ case SOURCE_ANYONE: return Policy.PRIORITY_SENDERS_ANY;
+ case SOURCE_CONTACT: return Policy.PRIORITY_SENDERS_CONTACTS;
+ case SOURCE_STAR: return Policy.PRIORITY_SENDERS_STARRED;
+ default: return def;
+ }
+ }
+
+ private static int prioritySendersToSource(int prioritySenders, int def) {
+ switch (prioritySenders) {
+ case Policy.PRIORITY_SENDERS_CONTACTS: return SOURCE_CONTACT;
+ case Policy.PRIORITY_SENDERS_STARRED: return SOURCE_STAR;
+ case Policy.PRIORITY_SENDERS_ANY: return SOURCE_ANYONE;
+ default: return def;
+ }
+ }
+
+ private static int normalizePrioritySenders(int prioritySenders, int def) {
+ if (!(prioritySenders == Policy.PRIORITY_SENDERS_CONTACTS
+ || prioritySenders == Policy.PRIORITY_SENDERS_STARRED
+ || prioritySenders == Policy.PRIORITY_SENDERS_ANY)) {
+ return def;
+ }
+ return prioritySenders;
+ }
+
+ private static int normalizeConversationSenders(boolean allowed, int senders, int def) {
+ if (!allowed) {
+ return CONVERSATION_SENDERS_NONE;
+ }
+ if (!(senders == CONVERSATION_SENDERS_ANYONE
+ || senders == CONVERSATION_SENDERS_IMPORTANT
+ || senders == CONVERSATION_SENDERS_NONE)) {
+ return def;
+ }
+ return senders;
+ }
+
+ public void applyNotificationPolicy(Policy policy) {
+ if (policy == null) return;
+ allowAlarms = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_ALARMS) != 0;
+ allowMedia = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_MEDIA) != 0;
+ allowSystem = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_SYSTEM) != 0;
+ allowEvents = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_EVENTS) != 0;
+ allowReminders = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_REMINDERS) != 0;
+ allowCalls = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_CALLS) != 0;
+ allowMessages = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_MESSAGES) != 0;
+ allowRepeatCallers = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_REPEAT_CALLERS)
+ != 0;
+ allowCallsFrom = normalizePrioritySenders(policy.priorityCallSenders, allowCallsFrom);
+ allowMessagesFrom = normalizePrioritySenders(policy.priorityMessageSenders,
+ allowMessagesFrom);
+ if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) {
+ suppressedVisualEffects = policy.suppressedVisualEffects;
+ }
+ allowConversations = (policy.priorityCategories
+ & Policy.PRIORITY_CATEGORY_CONVERSATIONS) != 0;
+ allowConversationsFrom = normalizeConversationSenders(allowConversations,
+ policy.priorityConversationSenders,
+ allowConversationsFrom);
+ if (policy.state != Policy.STATE_UNSET) {
+ areChannelsBypassingDnd = (policy.state & Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
+ }
+ }
+
+ public static Condition toTimeCondition(Context context, int minutesFromNow, int userHandle) {
+ return toTimeCondition(context, minutesFromNow, userHandle, false /*shortVersion*/);
+ }
+
+ public static Condition toTimeCondition(Context context, int minutesFromNow, int userHandle,
+ boolean shortVersion) {
+ final long now = System.currentTimeMillis();
+ final long millis = minutesFromNow == 0 ? ZERO_VALUE_MS : minutesFromNow * MINUTES_MS;
+ return toTimeCondition(context, now + millis, minutesFromNow, userHandle, shortVersion);
+ }
+
+ public static Condition toTimeCondition(Context context, long time, int minutes,
+ int userHandle, boolean shortVersion) {
+ final int num;
+ String summary, line1, line2;
+ final CharSequence formattedTime =
+ getFormattedTime(context, time, isToday(time), userHandle);
+ final Resources res = context.getResources();
+ if (minutes < 60) {
+ // display as minutes
+ num = minutes;
+ int summaryResId = shortVersion ? R.plurals.zen_mode_duration_minutes_summary_short
+ : R.plurals.zen_mode_duration_minutes_summary;
+ summary = res.getQuantityString(summaryResId, num, num, formattedTime);
+ int line1ResId = shortVersion ? R.plurals.zen_mode_duration_minutes_short
+ : R.plurals.zen_mode_duration_minutes;
+ line1 = res.getQuantityString(line1ResId, num, num, formattedTime);
+ line2 = res.getString(R.string.zen_mode_until, formattedTime);
+ } else if (minutes < DAY_MINUTES) {
+ // display as hours
+ num = Math.round(minutes / 60f);
+ int summaryResId = shortVersion ? R.plurals.zen_mode_duration_hours_summary_short
+ : R.plurals.zen_mode_duration_hours_summary;
+ summary = res.getQuantityString(summaryResId, num, num, formattedTime);
+ int line1ResId = shortVersion ? R.plurals.zen_mode_duration_hours_short
+ : R.plurals.zen_mode_duration_hours;
+ line1 = res.getQuantityString(line1ResId, num, num, formattedTime);
+ line2 = res.getString(R.string.zen_mode_until, formattedTime);
+ } else {
+ // display as day/time
+ summary = line1 = line2 = res.getString(R.string.zen_mode_until, formattedTime);
+ }
+ final Uri id = toCountdownConditionId(time, false);
+ return new Condition(id, summary, line1, line2, 0, Condition.STATE_TRUE,
+ Condition.FLAG_RELEVANT_NOW);
+ }
+
+ /**
+ * Converts countdown to alarm parameters into a condition with user facing summary
+ */
+ public static Condition toNextAlarmCondition(Context context, long alarm,
+ int userHandle) {
+ boolean isSameDay = isToday(alarm);
+ final CharSequence formattedTime = getFormattedTime(context, alarm, isSameDay, userHandle);
+ final Resources res = context.getResources();
+ final String line1 = res.getString(R.string.zen_mode_until, formattedTime);
+ final Uri id = toCountdownConditionId(alarm, true);
+ return new Condition(id, "", line1, "", 0, Condition.STATE_TRUE,
+ Condition.FLAG_RELEVANT_NOW);
+ }
+
+ /**
+ * Creates readable time from time in milliseconds
+ */
+ public static CharSequence getFormattedTime(Context context, long time, boolean isSameDay,
+ int userHandle) {
+ String skeleton = (!isSameDay ? "EEE " : "")
+ + (DateFormat.is24HourFormat(context, userHandle) ? "Hm" : "hma");
+ final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
+ return DateFormat.format(pattern, time);
+ }
+
+ /**
+ * Determines whether a time in milliseconds is today or not
+ */
+ public static boolean isToday(long time) {
+ GregorianCalendar now = new GregorianCalendar();
+ GregorianCalendar endTime = new GregorianCalendar();
+ endTime.setTimeInMillis(time);
+ if (now.get(Calendar.YEAR) == endTime.get(Calendar.YEAR)
+ && now.get(Calendar.MONTH) == endTime.get(Calendar.MONTH)
+ && now.get(Calendar.DATE) == endTime.get(Calendar.DATE)) {
+ return true;
+ }
+ return false;
+ }
+
+ // ==== Built-in system conditions ====
+
+ public static final String SYSTEM_AUTHORITY = "android";
+
+ // ==== Built-in system condition: countdown ====
+
+ public static final String COUNTDOWN_PATH = "countdown";
+
+ public static final String IS_ALARM_PATH = "alarm";
+
+ /**
+ * Converts countdown condition parameters into a condition id.
+ */
+ public static Uri toCountdownConditionId(long time, boolean alarm) {
+ return new Uri.Builder().scheme(Condition.SCHEME)
+ .authority(SYSTEM_AUTHORITY)
+ .appendPath(COUNTDOWN_PATH)
+ .appendPath(Long.toString(time))
+ .appendPath(IS_ALARM_PATH)
+ .appendPath(Boolean.toString(alarm))
+ .build();
+ }
+
+ public static long tryParseCountdownConditionId(Uri conditionId) {
+ if (!Condition.isValidId(conditionId, SYSTEM_AUTHORITY)) return 0;
+ if (conditionId.getPathSegments().size() < 2
+ || !COUNTDOWN_PATH.equals(conditionId.getPathSegments().get(0))) return 0;
+ try {
+ return Long.parseLong(conditionId.getPathSegments().get(1));
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Error parsing countdown condition: " + conditionId, e);
+ return 0;
+ }
+ }
+
+ /**
+ * Returns whether this condition is a countdown condition.
+ */
+ public static boolean isValidCountdownConditionId(Uri conditionId) {
+ return tryParseCountdownConditionId(conditionId) != 0;
+ }
+
+ /**
+ * Returns whether this condition is a countdown to an alarm.
+ */
+ public static boolean isValidCountdownToAlarmConditionId(Uri conditionId) {
+ if (tryParseCountdownConditionId(conditionId) != 0) {
+ if (conditionId.getPathSegments().size() < 4
+ || !IS_ALARM_PATH.equals(conditionId.getPathSegments().get(2))) {
+ return false;
+ }
+ try {
+ return Boolean.parseBoolean(conditionId.getPathSegments().get(3));
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Error parsing countdown alarm condition: " + conditionId, e);
+ return false;
+ }
+ }
+ return false;
+ }
+
+ // ==== Built-in system condition: schedule ====
+
+ public static final String SCHEDULE_PATH = "schedule";
+
+ public static Uri toScheduleConditionId(ScheduleInfo schedule) {
+ return new Uri.Builder().scheme(Condition.SCHEME)
+ .authority(SYSTEM_AUTHORITY)
+ .appendPath(SCHEDULE_PATH)
+ .appendQueryParameter("days", toDayList(schedule.days))
+ .appendQueryParameter("start", schedule.startHour + "." + schedule.startMinute)
+ .appendQueryParameter("end", schedule.endHour + "." + schedule.endMinute)
+ .appendQueryParameter("exitAtAlarm", String.valueOf(schedule.exitAtAlarm))
+ .build();
+ }
+
+ public static boolean isValidScheduleConditionId(Uri conditionId) {
+ ScheduleInfo info;
+ try {
+ info = tryParseScheduleConditionId(conditionId);
+ } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
+ return false;
+ }
+
+ if (info == null || info.days == null || info.days.length == 0) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether the conditionId is a valid ScheduleCondition.
+ * If allowNever is true, this will return true even if the ScheduleCondition never occurs.
+ */
+ public static boolean isValidScheduleConditionId(Uri conditionId, boolean allowNever) {
+ ScheduleInfo info;
+ try {
+ info = tryParseScheduleConditionId(conditionId);
+ } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
+ return false;
+ }
+
+ if (info == null || (!allowNever && (info.days == null || info.days.length == 0))) {
+ return false;
+ }
+ return true;
+ }
+
+ @UnsupportedAppUsage
+ public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) {
+ final boolean isSchedule = conditionId != null
+ && Condition.SCHEME.equals(conditionId.getScheme())
+ && ZenModeConfig.SYSTEM_AUTHORITY.equals(conditionId.getAuthority())
+ && conditionId.getPathSegments().size() == 1
+ && ZenModeConfig.SCHEDULE_PATH.equals(conditionId.getPathSegments().get(0));
+ if (!isSchedule) return null;
+ final int[] start = tryParseHourAndMinute(conditionId.getQueryParameter("start"));
+ final int[] end = tryParseHourAndMinute(conditionId.getQueryParameter("end"));
+ if (start == null || end == null) return null;
+ final ScheduleInfo rt = new ScheduleInfo();
+ rt.days = tryParseDayList(conditionId.getQueryParameter("days"), "\\.");
+ rt.startHour = start[0];
+ rt.startMinute = start[1];
+ rt.endHour = end[0];
+ rt.endMinute = end[1];
+ rt.exitAtAlarm = safeBoolean(conditionId.getQueryParameter("exitAtAlarm"), false);
+ return rt;
+ }
+
+ public static ComponentName getScheduleConditionProvider() {
+ return new ComponentName(SYSTEM_AUTHORITY, "ScheduleConditionProvider");
+ }
+
+ public static class ScheduleInfo {
+ @UnsupportedAppUsage
+ public int[] days;
+ @UnsupportedAppUsage
+ public int startHour;
+ @UnsupportedAppUsage
+ public int startMinute;
+ @UnsupportedAppUsage
+ public int endHour;
+ @UnsupportedAppUsage
+ public int endMinute;
+ public boolean exitAtAlarm;
+ public long nextAlarm;
+
+ @Override
+ public int hashCode() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ScheduleInfo)) return false;
+ final ScheduleInfo other = (ScheduleInfo) o;
+ return toDayList(days).equals(toDayList(other.days))
+ && startHour == other.startHour
+ && startMinute == other.startMinute
+ && endHour == other.endHour
+ && endMinute == other.endMinute
+ && exitAtAlarm == other.exitAtAlarm;
+ }
+
+ public ScheduleInfo copy() {
+ final ScheduleInfo rt = new ScheduleInfo();
+ if (days != null) {
+ rt.days = new int[days.length];
+ System.arraycopy(days, 0, rt.days, 0, days.length);
+ }
+ rt.startHour = startHour;
+ rt.startMinute = startMinute;
+ rt.endHour = endHour;
+ rt.endMinute = endMinute;
+ rt.exitAtAlarm = exitAtAlarm;
+ rt.nextAlarm = nextAlarm;
+ return rt;
+ }
+
+ @Override
+ public String toString() {
+ return "ScheduleInfo{" +
+ "days=" + Arrays.toString(days) +
+ ", startHour=" + startHour +
+ ", startMinute=" + startMinute +
+ ", endHour=" + endHour +
+ ", endMinute=" + endMinute +
+ ", exitAtAlarm=" + exitAtAlarm +
+ ", nextAlarm=" + ts(nextAlarm) +
+ '}';
+ }
+
+ protected static String ts(long time) {
+ return new Date(time) + " (" + time + ")";
+ }
+ }
+
+ // ==== Built-in system condition: event ====
+
+ public static final String EVENT_PATH = "event";
+
+ public static Uri toEventConditionId(EventInfo event) {
+ return new Uri.Builder().scheme(Condition.SCHEME)
+ .authority(SYSTEM_AUTHORITY)
+ .appendPath(EVENT_PATH)
+ .appendQueryParameter("userId", Long.toString(event.userId))
+ .appendQueryParameter("calendar", event.calName != null ? event.calName : "")
+ .appendQueryParameter("calendarId", event.calendarId != null
+ ? event.calendarId.toString() : "")
+ .appendQueryParameter("reply", Integer.toString(event.reply))
+ .build();
+ }
+
+ public static boolean isValidEventConditionId(Uri conditionId) {
+ return tryParseEventConditionId(conditionId) != null;
+ }
+
+ public static EventInfo tryParseEventConditionId(Uri conditionId) {
+ final boolean isEvent = conditionId != null
+ && Condition.SCHEME.equals(conditionId.getScheme())
+ && ZenModeConfig.SYSTEM_AUTHORITY.equals(conditionId.getAuthority())
+ && conditionId.getPathSegments().size() == 1
+ && EVENT_PATH.equals(conditionId.getPathSegments().get(0));
+ if (!isEvent) return null;
+ final EventInfo rt = new EventInfo();
+ rt.userId = tryParseInt(conditionId.getQueryParameter("userId"), UserHandle.USER_NULL);
+ rt.calName = conditionId.getQueryParameter("calendar");
+ if (TextUtils.isEmpty(rt.calName)) {
+ rt.calName = null;
+ }
+ rt.calendarId = tryParseLong(conditionId.getQueryParameter("calendarId"), null);
+ rt.reply = tryParseInt(conditionId.getQueryParameter("reply"), 0);
+ return rt;
+ }
+
+ public static ComponentName getEventConditionProvider() {
+ return new ComponentName(SYSTEM_AUTHORITY, "EventConditionProvider");
+ }
+
+ public static class EventInfo {
+ public static final int REPLY_ANY_EXCEPT_NO = 0;
+ public static final int REPLY_YES_OR_MAYBE = 1;
+ public static final int REPLY_YES = 2;
+
+ public int userId = UserHandle.USER_NULL; // USER_NULL = unspecified - use current user
+ public String calName; // CalendarContract.Calendars.DISPLAY_NAME, or null for any
+ public Long calendarId; // Calendars._ID, or null if restored from < Q calendar
+ public int reply;
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(userId, calName, calendarId, reply);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof EventInfo)) return false;
+ final EventInfo other = (EventInfo) o;
+ return userId == other.userId
+ && Objects.equals(calName, other.calName)
+ && reply == other.reply
+ && Objects.equals(calendarId, other.calendarId);
+ }
+
+ public EventInfo copy() {
+ final EventInfo rt = new EventInfo();
+ rt.userId = userId;
+ rt.calName = calName;
+ rt.reply = reply;
+ rt.calendarId = calendarId;
+ return rt;
+ }
+
+ public static int resolveUserId(int userId) {
+ return userId == UserHandle.USER_NULL ? ActivityManager.getCurrentUser() : userId;
+ }
+ }
+
+ // ==== End built-in system conditions ====
+
+ private static int[] tryParseHourAndMinute(String value) {
+ if (TextUtils.isEmpty(value)) return null;
+ final int i = value.indexOf('.');
+ if (i < 1 || i >= value.length() - 1) return null;
+ final int hour = tryParseInt(value.substring(0, i), -1);
+ final int minute = tryParseInt(value.substring(i + 1), -1);
+ return isValidHour(hour) && isValidMinute(minute) ? new int[] { hour, minute } : null;
+ }
+
+ private static int tryParseZenMode(String value, int defValue) {
+ final int rt = tryParseInt(value, defValue);
+ return Global.isValidZenMode(rt) ? rt : defValue;
+ }
+
+ public static String newRuleId() {
+ return UUID.randomUUID().toString().replace("-", "");
+ }
+
+ /**
+ * Gets the name of the app associated with owner
+ */
+ public static String getOwnerCaption(Context context, String owner) {
+ final PackageManager pm = context.getPackageManager();
+ try {
+ final ApplicationInfo info = pm.getApplicationInfo(owner, 0);
+ if (info != null) {
+ final CharSequence seq = info.loadLabel(pm);
+ if (seq != null) {
+ final String str = seq.toString().trim();
+ if (str.length() > 0) {
+ return str;
+ }
+ }
+ }
+ } catch (Throwable e) {
+ Slog.w(TAG, "Error loading owner caption", e);
+ }
+ return "";
+ }
+
+ public static String getConditionSummary(Context context, ZenModeConfig config,
+ int userHandle, boolean shortVersion) {
+ return getConditionLine(context, config, userHandle, false /*useLine1*/, shortVersion);
+ }
+
+ private static String getConditionLine(Context context, ZenModeConfig config,
+ int userHandle, boolean useLine1, boolean shortVersion) {
+ if (config == null) return "";
+ String summary = "";
+ if (config.manualRule != null) {
+ final Uri id = config.manualRule.conditionId;
+ if (config.manualRule.enabler != null) {
+ summary = getOwnerCaption(context, config.manualRule.enabler);
+ } else {
+ if (id == null) {
+ summary = context.getString(com.android.internal.R.string.zen_mode_forever);
+ } else {
+ final long time = tryParseCountdownConditionId(id);
+ Condition c = config.manualRule.condition;
+ if (time > 0) {
+ final long now = System.currentTimeMillis();
+ final long span = time - now;
+ c = toTimeCondition(context, time, Math.round(span / (float) MINUTES_MS),
+ userHandle, shortVersion);
+ }
+ final String rt = c == null ? "" : useLine1 ? c.line1 : c.summary;
+ summary = TextUtils.isEmpty(rt) ? "" : rt;
+ }
+ }
+ }
+ for (ZenRule automaticRule : config.automaticRules.values()) {
+ if (automaticRule.isAutomaticActive()) {
+ if (summary.isEmpty()) {
+ summary = automaticRule.name;
+ } else {
+ summary = context.getResources()
+ .getString(R.string.zen_mode_rule_name_combination, summary,
+ automaticRule.name);
+ }
+
+ }
+ }
+ return summary;
+ }
+
+ public static class ZenRule implements Parcelable {
+ @UnsupportedAppUsage
+ public boolean enabled;
+ @UnsupportedAppUsage
+ public boolean snoozing; // user manually disabled this instance
+ @UnsupportedAppUsage
+ public String name; // required for automatic
+ @UnsupportedAppUsage
+ public int zenMode; // ie: Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+ @UnsupportedAppUsage
+ public Uri conditionId; // required for automatic
+ public Condition condition; // optional
+ public ComponentName component; // optional
+ public ComponentName configurationActivity; // optional
+ public String id; // required for automatic (unique)
+ @UnsupportedAppUsage
+ public long creationTime; // required for automatic
+ // package name, only used for manual rules when they have turned DND on.
+ public String enabler;
+ public ZenPolicy zenPolicy;
+ public boolean modified; // rule has been modified from initial creation
+ public String pkg;
+
+ public ZenRule() { }
+
+ public ZenRule(Parcel source) {
+ enabled = source.readInt() == 1;
+ snoozing = source.readInt() == 1;
+ if (source.readInt() == 1) {
+ name = source.readString();
+ }
+ zenMode = source.readInt();
+ conditionId = source.readParcelable(null);
+ condition = source.readParcelable(null);
+ component = source.readParcelable(null);
+ configurationActivity = source.readParcelable(null);
+ if (source.readInt() == 1) {
+ id = source.readString();
+ }
+ creationTime = source.readLong();
+ if (source.readInt() == 1) {
+ enabler = source.readString();
+ }
+ zenPolicy = source.readParcelable(null);
+ modified = source.readInt() == 1;
+ pkg = source.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(enabled ? 1 : 0);
+ dest.writeInt(snoozing ? 1 : 0);
+ if (name != null) {
+ dest.writeInt(1);
+ dest.writeString(name);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(zenMode);
+ dest.writeParcelable(conditionId, 0);
+ dest.writeParcelable(condition, 0);
+ dest.writeParcelable(component, 0);
+ dest.writeParcelable(configurationActivity, 0);
+ if (id != null) {
+ dest.writeInt(1);
+ dest.writeString(id);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeLong(creationTime);
+ if (enabler != null) {
+ dest.writeInt(1);
+ dest.writeString(enabler);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeParcelable(zenPolicy, 0);
+ dest.writeInt(modified ? 1 : 0);
+ dest.writeString(pkg);
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(ZenRule.class.getSimpleName()).append('[')
+ .append("id=").append(id)
+ .append(",enabled=").append(String.valueOf(enabled).toUpperCase())
+ .append(",snoozing=").append(snoozing)
+ .append(",name=").append(name)
+ .append(",zenMode=").append(Global.zenModeToString(zenMode))
+ .append(",conditionId=").append(conditionId)
+ .append(",condition=").append(condition)
+ .append(",pkg=").append(pkg)
+ .append(",component=").append(component)
+ .append(",configActivity=").append(configurationActivity)
+ .append(",creationTime=").append(creationTime)
+ .append(",enabler=").append(enabler)
+ .append(",zenPolicy=").append(zenPolicy)
+ .append(",modified=").append(modified)
+ .append(']').toString();
+ }
+
+ /** @hide */
+ // TODO: add configuration activity
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(ZenRuleProto.ID, id);
+ proto.write(ZenRuleProto.NAME, name);
+ proto.write(ZenRuleProto.CREATION_TIME_MS, creationTime);
+ proto.write(ZenRuleProto.ENABLED, enabled);
+ proto.write(ZenRuleProto.ENABLER, enabler);
+ proto.write(ZenRuleProto.IS_SNOOZING, snoozing);
+ proto.write(ZenRuleProto.ZEN_MODE, zenMode);
+ if (conditionId != null) {
+ proto.write(ZenRuleProto.CONDITION_ID, conditionId.toString());
+ }
+ if (condition != null) {
+ condition.dumpDebug(proto, ZenRuleProto.CONDITION);
+ }
+ if (component != null) {
+ component.dumpDebug(proto, ZenRuleProto.COMPONENT);
+ }
+ if (zenPolicy != null) {
+ zenPolicy.dumpDebug(proto, ZenRuleProto.ZEN_POLICY);
+ }
+ proto.write(ZenRuleProto.MODIFIED, modified);
+ proto.end(token);
+ }
+
+ private static void appendDiff(Diff d, String item, ZenRule from, ZenRule to) {
+ if (d == null) return;
+ if (from == null) {
+ if (to != null) {
+ d.addLine(item, "insert");
+ }
+ return;
+ }
+ from.appendDiff(d, item, to);
+ }
+
+ private void appendDiff(Diff d, String item, ZenRule to) {
+ if (to == null) {
+ d.addLine(item, "delete");
+ return;
+ }
+ if (enabled != to.enabled) {
+ d.addLine(item, "enabled", enabled, to.enabled);
+ }
+ if (snoozing != to.snoozing) {
+ d.addLine(item, "snoozing", snoozing, to.snoozing);
+ }
+ if (!Objects.equals(name, to.name)) {
+ d.addLine(item, "name", name, to.name);
+ }
+ if (zenMode != to.zenMode) {
+ d.addLine(item, "zenMode", zenMode, to.zenMode);
+ }
+ if (!Objects.equals(conditionId, to.conditionId)) {
+ d.addLine(item, "conditionId", conditionId, to.conditionId);
+ }
+ if (!Objects.equals(condition, to.condition)) {
+ d.addLine(item, "condition", condition, to.condition);
+ }
+ if (!Objects.equals(component, to.component)) {
+ d.addLine(item, "component", component, to.component);
+ }
+ if (!Objects.equals(configurationActivity, to.configurationActivity)) {
+ d.addLine(item, "configActivity", configurationActivity, to.configurationActivity);
+ }
+ if (!Objects.equals(id, to.id)) {
+ d.addLine(item, "id", id, to.id);
+ }
+ if (creationTime != to.creationTime) {
+ d.addLine(item, "creationTime", creationTime, to.creationTime);
+ }
+ if (!Objects.equals(enabler, to.enabler)) {
+ d.addLine(item, "enabler", enabler, to.enabler);
+ }
+ if (!Objects.equals(zenPolicy, to.zenPolicy)) {
+ d.addLine(item, "zenPolicy", zenPolicy, to.zenPolicy);
+ }
+ if (modified != to.modified) {
+ d.addLine(item, "modified", modified, to.modified);
+ }
+ if (!Objects.equals(pkg, to.pkg)) {
+ d.addLine(item, "pkg", pkg, to.pkg);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ZenRule)) return false;
+ if (o == this) return true;
+ final ZenRule other = (ZenRule) o;
+ return other.enabled == enabled
+ && other.snoozing == snoozing
+ && Objects.equals(other.name, name)
+ && other.zenMode == zenMode
+ && Objects.equals(other.conditionId, conditionId)
+ && Objects.equals(other.condition, condition)
+ && Objects.equals(other.component, component)
+ && Objects.equals(other.configurationActivity, configurationActivity)
+ && Objects.equals(other.id, id)
+ && Objects.equals(other.enabler, enabler)
+ && Objects.equals(other.zenPolicy, zenPolicy)
+ && Objects.equals(other.pkg, pkg)
+ && other.modified == modified;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
+ component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
+ }
+
+ public boolean isAutomaticActive() {
+ return enabled && !snoozing && pkg != null && isTrueOrUnknown();
+ }
+
+ public boolean isTrueOrUnknown() {
+ return condition != null && (condition.state == Condition.STATE_TRUE
+ || condition.state == Condition.STATE_UNKNOWN);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ZenRule> CREATOR
+ = new Parcelable.Creator<ZenRule>() {
+ @Override
+ public ZenRule createFromParcel(Parcel source) {
+ return new ZenRule(source);
+ }
+ @Override
+ public ZenRule[] newArray(int size) {
+ return new ZenRule[size];
+ }
+ };
+ }
+
+ public static class Diff {
+ private final ArrayList<String> lines = new ArrayList<>();
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("Diff[");
+ final int N = lines.size();
+ for (int i = 0; i < N; i++) {
+ if (i > 0) {
+ sb.append(",\n");
+ }
+ sb.append(lines.get(i));
+ }
+ return sb.append(']').toString();
+ }
+
+ private Diff addLine(String item, String action) {
+ lines.add(item + ":" + action);
+ return this;
+ }
+
+ public Diff addLine(String item, String subitem, Object from, Object to) {
+ return addLine(item + "." + subitem, from, to);
+ }
+
+ public Diff addLine(String item, Object from, Object to) {
+ return addLine(item, from + "->" + to);
+ }
+ }
+
+ /**
+ * Determines whether dnd behavior should mute all ringer-controlled sounds
+ * This includes notification, ringer and system sounds
+ */
+ public static boolean areAllPriorityOnlyRingerSoundsMuted(NotificationManager.Policy
+ policy) {
+ boolean allowReminders = (policy.priorityCategories
+ & NotificationManager.Policy.PRIORITY_CATEGORY_REMINDERS) != 0;
+ boolean allowCalls = (policy.priorityCategories
+ & NotificationManager.Policy.PRIORITY_CATEGORY_CALLS) != 0;
+ boolean allowMessages = (policy.priorityCategories
+ & NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES) != 0;
+ boolean allowEvents = (policy.priorityCategories
+ & NotificationManager.Policy.PRIORITY_CATEGORY_EVENTS) != 0;
+ boolean allowRepeatCallers = (policy.priorityCategories
+ & NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS) != 0;
+ boolean allowConversations = (policy.priorityConversationSenders
+ & Policy.PRIORITY_CATEGORY_CONVERSATIONS) != 0;
+ boolean areChannelsBypassingDnd = (policy.state & Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
+ boolean allowSystem = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_SYSTEM) != 0;
+ return !allowReminders && !allowCalls && !allowMessages && !allowEvents
+ && !allowRepeatCallers && !areChannelsBypassingDnd && !allowSystem
+ && !allowConversations;
+ }
+
+ /**
+ * Determines whether dnd behavior should mute all sounds
+ */
+ public static boolean areAllZenBehaviorSoundsMuted(NotificationManager.Policy
+ policy) {
+ boolean allowAlarms = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_ALARMS) != 0;
+ boolean allowMedia = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_MEDIA) != 0;
+ return !allowAlarms && !allowMedia && areAllPriorityOnlyRingerSoundsMuted(policy);
+ }
+
+ /**
+ * Determines if DND is currently overriding the ringer
+ */
+ public static boolean isZenOverridingRinger(int zen, Policy consolidatedPolicy) {
+ return zen == Global.ZEN_MODE_NO_INTERRUPTIONS
+ || zen == Global.ZEN_MODE_ALARMS
+ || (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+ && ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(consolidatedPolicy));
+ }
+
+ /**
+ * Determines whether dnd behavior should mute all ringer-controlled sounds
+ * This includes notification, ringer and system sounds
+ */
+ public static boolean areAllPriorityOnlyRingerSoundsMuted(ZenModeConfig config) {
+ return !config.allowReminders && !config.allowCalls && !config.allowMessages
+ && !config.allowEvents && !config.allowRepeatCallers
+ && !config.areChannelsBypassingDnd && !config.allowSystem;
+ }
+
+ /**
+ * Determines whether dnd mutes all sounds
+ */
+ public static boolean areAllZenBehaviorSoundsMuted(ZenModeConfig config) {
+ return !config.allowAlarms && !config.allowMedia
+ && areAllPriorityOnlyRingerSoundsMuted(config);
+ }
+
+ /**
+ * Returns a description of the current do not disturb settings from config.
+ * - If turned on manually and end time is known, returns end time.
+ * - If turned on manually and end time is on forever until turned off, return null if
+ * describeForeverCondition is false, else return String describing indefinite behavior
+ * - If turned on by an automatic rule, returns the automatic rule name.
+ * - If on due to an app, returns the app name.
+ * - If there's a combination of rules/apps that trigger, then shows the one that will
+ * last the longest if applicable.
+ * @return null if DND is off or describeForeverCondition is false and
+ * DND is on forever (until turned off)
+ */
+ public static String getDescription(Context context, boolean zenOn, ZenModeConfig config,
+ boolean describeForeverCondition) {
+ if (!zenOn || config == null) {
+ return null;
+ }
+
+ String secondaryText = "";
+ long latestEndTime = -1;
+
+ // DND turned on by manual rule
+ if (config.manualRule != null) {
+ final Uri id = config.manualRule.conditionId;
+ if (config.manualRule.enabler != null) {
+ // app triggered manual rule
+ String appName = getOwnerCaption(context, config.manualRule.enabler);
+ if (!appName.isEmpty()) {
+ secondaryText = appName;
+ }
+ } else {
+ if (id == null) {
+ // Do not disturb manually triggered to remain on forever until turned off
+ if (describeForeverCondition) {
+ return context.getString(R.string.zen_mode_forever);
+ } else {
+ return null;
+ }
+ } else {
+ latestEndTime = tryParseCountdownConditionId(id);
+ if (latestEndTime > 0) {
+ final CharSequence formattedTime = getFormattedTime(context,
+ latestEndTime, isToday(latestEndTime),
+ context.getUserId());
+ secondaryText = context.getString(R.string.zen_mode_until, formattedTime);
+ }
+ }
+ }
+ }
+
+ // DND turned on by an automatic rule
+ for (ZenRule automaticRule : config.automaticRules.values()) {
+ if (automaticRule.isAutomaticActive()) {
+ if (isValidEventConditionId(automaticRule.conditionId)
+ || isValidScheduleConditionId(automaticRule.conditionId)) {
+ // set text if automatic rule end time is the latest active rule end time
+ long endTime = parseAutomaticRuleEndTime(context, automaticRule.conditionId);
+ if (endTime > latestEndTime) {
+ latestEndTime = endTime;
+ secondaryText = automaticRule.name;
+ }
+ } else {
+ // set text if 3rd party rule
+ return automaticRule.name;
+ }
+ }
+ }
+
+ return !secondaryText.equals("") ? secondaryText : null;
+ }
+
+ private static long parseAutomaticRuleEndTime(Context context, Uri id) {
+ if (isValidEventConditionId(id)) {
+ // cannot look up end times for events
+ return Long.MAX_VALUE;
+ }
+
+ if (isValidScheduleConditionId(id)) {
+ ScheduleCalendar schedule = toScheduleCalendar(id);
+ long endTimeMs = schedule.getNextChangeTime(System.currentTimeMillis());
+
+ // check if automatic rule will end on next alarm
+ if (schedule.exitAtAlarm()) {
+ long nextAlarm = getNextAlarm(context);
+ schedule.maybeSetNextAlarm(System.currentTimeMillis(), nextAlarm);
+ if (schedule.shouldExitForAlarm(endTimeMs)) {
+ return nextAlarm;
+ }
+ }
+
+ return endTimeMs;
+ }
+
+ return -1;
+ }
+
+ private static long getNextAlarm(Context context) {
+ final AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ final AlarmManager.AlarmClockInfo info = alarms.getNextAlarmClock(context.getUserId());
+ return info != null ? info.getTriggerTime() : 0;
+ }
+}
diff --git a/android/service/notification/ZenPolicy.java b/android/service/notification/ZenPolicy.java
new file mode 100644
index 0000000..87295e1
--- /dev/null
+++ b/android/service/notification/ZenPolicy.java
@@ -0,0 +1,1127 @@
+/*
+ * Copyright (C) 2018 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.notification;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.proto.ProtoOutputStream;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * ZenPolicy determines whether to allow certain notifications and their corresponding sounds to
+ * play when a device is in Do Not Disturb mode.
+ * ZenPolicy also dictates the visual effects of notifications that are intercepted when
+ * a device is in Do Not Disturb mode.
+ */
+public final class ZenPolicy implements Parcelable {
+ private ArrayList<Integer> mPriorityCategories;
+ private ArrayList<Integer> mVisualEffects;
+ private @PeopleType int mPriorityMessages = PEOPLE_TYPE_UNSET;
+ private @PeopleType int mPriorityCalls = PEOPLE_TYPE_UNSET;
+ private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET;
+
+ /** @hide */
+ @IntDef(prefix = { "PRIORITY_CATEGORY_" }, value = {
+ PRIORITY_CATEGORY_REMINDERS,
+ PRIORITY_CATEGORY_EVENTS,
+ PRIORITY_CATEGORY_MESSAGES,
+ PRIORITY_CATEGORY_CALLS,
+ PRIORITY_CATEGORY_REPEAT_CALLERS,
+ PRIORITY_CATEGORY_ALARMS,
+ PRIORITY_CATEGORY_MEDIA,
+ PRIORITY_CATEGORY_SYSTEM,
+ PRIORITY_CATEGORY_CONVERSATIONS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PriorityCategory {}
+
+ /** @hide */
+ public static final int PRIORITY_CATEGORY_REMINDERS = 0;
+ /** @hide */
+ public static final int PRIORITY_CATEGORY_EVENTS = 1;
+ /** @hide */
+ public static final int PRIORITY_CATEGORY_MESSAGES = 2;
+ /** @hide */
+ public static final int PRIORITY_CATEGORY_CALLS = 3;
+ /** @hide */
+ public static final int PRIORITY_CATEGORY_REPEAT_CALLERS = 4;
+ /** @hide */
+ public static final int PRIORITY_CATEGORY_ALARMS = 5;
+ /** @hide */
+ public static final int PRIORITY_CATEGORY_MEDIA = 6;
+ /** @hide */
+ public static final int PRIORITY_CATEGORY_SYSTEM = 7;
+ /** @hide */
+ public static final int PRIORITY_CATEGORY_CONVERSATIONS = 8;
+
+ /** @hide */
+ @IntDef(prefix = { "VISUAL_EFFECT_" }, value = {
+ VISUAL_EFFECT_FULL_SCREEN_INTENT,
+ VISUAL_EFFECT_LIGHTS,
+ VISUAL_EFFECT_PEEK,
+ VISUAL_EFFECT_STATUS_BAR,
+ VISUAL_EFFECT_BADGE,
+ VISUAL_EFFECT_AMBIENT,
+ VISUAL_EFFECT_NOTIFICATION_LIST,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface VisualEffect {}
+
+ /** @hide */
+ public static final int VISUAL_EFFECT_FULL_SCREEN_INTENT = 0;
+ /** @hide */
+ public static final int VISUAL_EFFECT_LIGHTS = 1;
+ /** @hide */
+ public static final int VISUAL_EFFECT_PEEK = 2;
+ /** @hide */
+ public static final int VISUAL_EFFECT_STATUS_BAR = 3;
+ /** @hide */
+ public static final int VISUAL_EFFECT_BADGE = 4;
+ /** @hide */
+ public static final int VISUAL_EFFECT_AMBIENT = 5;
+ /** @hide */
+ public static final int VISUAL_EFFECT_NOTIFICATION_LIST = 6;
+
+ /** @hide */
+ @IntDef(prefix = { "PEOPLE_TYPE_" }, value = {
+ PEOPLE_TYPE_UNSET,
+ PEOPLE_TYPE_ANYONE,
+ PEOPLE_TYPE_CONTACTS,
+ PEOPLE_TYPE_STARRED,
+ PEOPLE_TYPE_NONE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PeopleType {}
+
+ /**
+ * Used to indicate no preference for the type of people that can bypass dnd for either
+ * calls or messages.
+ */
+ public static final int PEOPLE_TYPE_UNSET = 0;
+
+ /**
+ * Used to indicate all calls or messages can bypass dnd.
+ */
+ public static final int PEOPLE_TYPE_ANYONE = 1;
+
+ /**
+ * Used to indicate calls or messages from contacts can bypass dnd.
+ */
+ public static final int PEOPLE_TYPE_CONTACTS = 2;
+
+ /**
+ * Used to indicate calls or messages from starred contacts can bypass dnd.
+ */
+ public static final int PEOPLE_TYPE_STARRED = 3;
+
+ /**
+ * Used to indicate no calls or messages can bypass dnd.
+ */
+ public static final int PEOPLE_TYPE_NONE = 4;
+
+
+ /** @hide */
+ @IntDef(prefix = { "CONVERSATION_SENDERS_" }, value = {
+ CONVERSATION_SENDERS_UNSET,
+ CONVERSATION_SENDERS_ANYONE,
+ CONVERSATION_SENDERS_IMPORTANT,
+ CONVERSATION_SENDERS_NONE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ConversationSenders {}
+
+ /**
+ * Used to indicate no preference for the type of conversations that can bypass dnd.
+ */
+ public static final int CONVERSATION_SENDERS_UNSET = 0;
+
+ /**
+ * Used to indicate all conversations can bypass dnd.
+ */
+ public static final int CONVERSATION_SENDERS_ANYONE = 1;
+
+ /**
+ * Used to indicate important conversations can bypass dnd.
+ */
+ public static final int CONVERSATION_SENDERS_IMPORTANT = 2;
+
+ /**
+ * Used to indicate no conversations can bypass dnd.
+ */
+ public static final int CONVERSATION_SENDERS_NONE = 3;
+
+ /** @hide */
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_UNSET,
+ STATE_ALLOW,
+ STATE_DISALLOW,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface State {}
+
+ /**
+ * Indicates no preference for whether a type of sound or visual effect is or isn't allowed
+ * to play/show when DND is active. Will default to the current set policy.
+ */
+ public static final int STATE_UNSET = 0;
+
+ /**
+ * Indicates a type of sound or visual effect is allowed to play/show when DND is active.
+ */
+ public static final int STATE_ALLOW = 1;
+
+ /**
+ * Indicates a type of sound or visual effect is not allowed to play/show when DND is active.
+ */
+ public static final int STATE_DISALLOW = 2;
+
+ /** @hide */
+ public ZenPolicy() {
+ mPriorityCategories = new ArrayList<>(Collections.nCopies(9, 0));
+ mVisualEffects = new ArrayList<>(Collections.nCopies(7, 0));
+ }
+
+ /**
+ * Conversation type that can bypass DND.
+ * @return {@link #CONVERSATION_SENDERS_UNSET}, {@link #CONVERSATION_SENDERS_ANYONE},
+ * {@link #CONVERSATION_SENDERS_IMPORTANT}, {@link #CONVERSATION_SENDERS_NONE}.
+ */
+ public @PeopleType int getPriorityConversationSenders() {
+ return mConversationSenders;
+ }
+
+ /**
+ * Message senders that can bypass DND.
+ * @return {@link #PEOPLE_TYPE_UNSET}, {@link #PEOPLE_TYPE_ANYONE},
+ * {@link #PEOPLE_TYPE_CONTACTS}, {@link #PEOPLE_TYPE_STARRED} or {@link #PEOPLE_TYPE_NONE}
+ */
+ public @PeopleType int getPriorityMessageSenders() {
+ return mPriorityMessages;
+ }
+
+ /**
+ * Callers that can bypass DND.
+ * @return {@link #PEOPLE_TYPE_UNSET}, {@link #PEOPLE_TYPE_ANYONE},
+ * {@link #PEOPLE_TYPE_CONTACTS}, {@link #PEOPLE_TYPE_STARRED} or {@link #PEOPLE_TYPE_NONE}
+ */
+ public @PeopleType int getPriorityCallSenders() {
+ return mPriorityCalls;
+ }
+
+ /**
+ * Whether this policy wants to allow conversation notifications
+ * (see {@link NotificationChannel#getConversationId()}) to play sounds and visually appear
+ * or to intercept them when DND is active.
+ * @return {@link #STATE_UNSET}, {@link #STATE_ALLOW} or {@link #STATE_DISALLOW}
+ */
+ public @State int getPriorityCategoryConversations() {
+ return mPriorityCategories.get(PRIORITY_CATEGORY_CONVERSATIONS);
+ }
+
+ /**
+ * Whether this policy wants to allow notifications with category
+ * {@link Notification#CATEGORY_REMINDER} to play sounds and visually appear
+ * or to intercept them when DND is active.
+ * @return {@link #STATE_UNSET}, {@link #STATE_ALLOW} or {@link #STATE_DISALLOW}
+ */
+ public @State int getPriorityCategoryReminders() {
+ return mPriorityCategories.get(PRIORITY_CATEGORY_REMINDERS);
+ }
+
+ /**
+ * Whether this policy wants to allow notifications with category
+ * {@link Notification#CATEGORY_EVENT} to play sounds and visually appear
+ * or to intercept them when DND is active.
+ * @return {@link #STATE_UNSET}, {@link #STATE_ALLOW} or {@link #STATE_DISALLOW}
+ */
+ public @State int getPriorityCategoryEvents() {
+ return mPriorityCategories.get(PRIORITY_CATEGORY_EVENTS);
+ }
+
+ /**
+ * Whether this policy wants to allow notifications with category
+ * {@link Notification#CATEGORY_MESSAGE} to play sounds and visually appear
+ * or to intercept them when DND is active. Types of message senders that are allowed
+ * are specified by {@link #getPriorityMessageSenders}.
+ * @return {@link #STATE_UNSET}, {@link #STATE_ALLOW} or {@link #STATE_DISALLOW}
+ */
+ public @State int getPriorityCategoryMessages() {
+ return mPriorityCategories.get(PRIORITY_CATEGORY_MESSAGES);
+ }
+
+ /**
+ * Whether this policy wants to allow notifications with category
+ * {@link Notification#CATEGORY_CALL} to play sounds and visually appear
+ * or to intercept them when DND is active. Types of callers that are allowed
+ * are specified by {@link #getPriorityCallSenders()}.
+ * @return {@link #STATE_UNSET}, {@link #STATE_ALLOW} or {@link #STATE_DISALLOW}
+ */
+ public @State int getPriorityCategoryCalls() {
+ return mPriorityCategories.get(PRIORITY_CATEGORY_CALLS);
+ }
+
+ /**
+ * Whether this policy wants to allow repeat callers (notifications with category
+ * {@link Notification#CATEGORY_CALL} that have recently called) to play sounds and
+ * visually appear or to intercept them when DND is active.
+ * @return {@link #STATE_UNSET}, {@link #STATE_ALLOW} or {@link #STATE_DISALLOW}
+ */
+ public @State int getPriorityCategoryRepeatCallers() {
+ return mPriorityCategories.get(PRIORITY_CATEGORY_REPEAT_CALLERS);
+ }
+
+ /**
+ * Whether this policy wants to allow notifications with category
+ * {@link Notification#CATEGORY_ALARM} to play sounds and visually appear
+ * or to intercept them when DND is active.
+ * When alarms are {@link #STATE_DISALLOW disallowed}, the alarm stream will be muted when DND
+ * is active.
+ * @return {@link #STATE_UNSET}, {@link #STATE_ALLOW} or {@link #STATE_DISALLOW}
+ */
+ public @State int getPriorityCategoryAlarms() {
+ return mPriorityCategories.get(PRIORITY_CATEGORY_ALARMS);
+ }
+
+ /**
+ * Whether this policy wants to allow media notifications to play sounds and visually appear
+ * or to intercept them when DND is active.
+ * When media is {@link #STATE_DISALLOW disallowed}, the media stream will be muted when DND is
+ * active.
+ * @return {@link #STATE_UNSET}, {@link #STATE_ALLOW} or {@link #STATE_DISALLOW}
+ */
+ public @State int getPriorityCategoryMedia() {
+ return mPriorityCategories.get(PRIORITY_CATEGORY_MEDIA);
+ }
+
+ /**
+ * Whether this policy wants to allow system sounds when DND is active.
+ * When system is {@link #STATE_DISALLOW}, the system stream will be muted when DND is active.
+ * @return {@link #STATE_UNSET}, {@link #STATE_ALLOW} or {@link #STATE_DISALLOW}
+ */
+ public @State int getPriorityCategorySystem() {
+ return mPriorityCategories.get(PRIORITY_CATEGORY_SYSTEM);
+ }
+
+ /**
+ * Whether this policy allows {@link Notification#fullScreenIntent full screen intents} from
+ * notifications intercepted by DND.
+ */
+ public @State int getVisualEffectFullScreenIntent() {
+ return mVisualEffects.get(VISUAL_EFFECT_FULL_SCREEN_INTENT);
+ }
+
+ /**
+ * Whether this policy allows {@link NotificationChannel#shouldShowLights() notification
+ * lights} from notifications intercepted by DND.
+ */
+ public @State int getVisualEffectLights() {
+ return mVisualEffects.get(VISUAL_EFFECT_LIGHTS);
+ }
+
+ /**
+ * Whether this policy allows peeking from notifications intercepted by DND.
+ */
+ public @State int getVisualEffectPeek() {
+ return mVisualEffects.get(VISUAL_EFFECT_PEEK);
+ }
+
+ /**
+ * Whether this policy allows notifications intercepted by DND from appearing in the status bar
+ * on devices that support status bars.
+ */
+ public @State int getVisualEffectStatusBar() {
+ return mVisualEffects.get(VISUAL_EFFECT_STATUS_BAR);
+ }
+
+ /**
+ * Whether this policy allows {@link NotificationChannel#canShowBadge() badges} from
+ * notifications intercepted by DND on devices that support badging.
+ */
+ public @State int getVisualEffectBadge() {
+ return mVisualEffects.get(VISUAL_EFFECT_BADGE);
+ }
+
+ /**
+ * Whether this policy allows notifications intercepted by DND from appearing on ambient
+ * displays on devices that support ambient display.
+ */
+ public @State int getVisualEffectAmbient() {
+ return mVisualEffects.get(VISUAL_EFFECT_AMBIENT);
+ }
+
+ /**
+ * Whether this policy allows notifications intercepted by DND from appearing in notification
+ * list views like the notification shade or lockscreen on devices that support those
+ * views.
+ */
+ public @State int getVisualEffectNotificationList() {
+ return mVisualEffects.get(VISUAL_EFFECT_NOTIFICATION_LIST);
+ }
+
+ /**
+ * Whether this policy hides all visual effects
+ * @hide
+ */
+ public boolean shouldHideAllVisualEffects() {
+ for (int i = 0; i < mVisualEffects.size(); i++) {
+ if (mVisualEffects.get(i) != STATE_DISALLOW) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Whether this policy shows all visual effects
+ * @hide
+ */
+ public boolean shouldShowAllVisualEffects() {
+ for (int i = 0; i < mVisualEffects.size(); i++) {
+ if (mVisualEffects.get(i) != STATE_ALLOW) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Builder class for {@link ZenPolicy} objects.
+ * Provides a convenient way to set the various fields of a {@link ZenPolicy}. If a field
+ * is not set, it is (@link STATE_UNSET} and will not change the current set policy.
+ */
+ public static final class Builder {
+ private ZenPolicy mZenPolicy;
+
+ public Builder() {
+ mZenPolicy = new ZenPolicy();
+ }
+
+ /**
+ * @hide
+ */
+ public Builder(ZenPolicy policy) {
+ if (policy != null) {
+ mZenPolicy = policy.copy();
+ } else {
+ mZenPolicy = new ZenPolicy();
+ }
+ }
+
+ /**
+ * Builds the current ZenPolicy.
+ */
+ public @NonNull ZenPolicy build() {
+ return mZenPolicy.copy();
+ }
+
+ /**
+ * Allows all notifications to bypass DND and unmutes all streams.
+ */
+ public @NonNull Builder allowAllSounds() {
+ for (int i = 0; i < mZenPolicy.mPriorityCategories.size(); i++) {
+ mZenPolicy.mPriorityCategories.set(i, STATE_ALLOW);
+ }
+ mZenPolicy.mPriorityMessages = PEOPLE_TYPE_ANYONE;
+ mZenPolicy.mPriorityCalls = PEOPLE_TYPE_ANYONE;
+ mZenPolicy.mConversationSenders = CONVERSATION_SENDERS_ANYONE;
+ return this;
+ }
+
+ /**
+ * Intercepts all notifications and prevents them from playing sounds
+ * when DND is active. Also mutes alarm, system and media streams.
+ * Notification channels can still play sounds only if they
+ * {@link NotificationChannel#canBypassDnd can bypass DND}. If no channels can bypass DND,
+ * the ringer stream is also muted.
+ */
+ public @NonNull Builder disallowAllSounds() {
+ for (int i = 0; i < mZenPolicy.mPriorityCategories.size(); i++) {
+ mZenPolicy.mPriorityCategories.set(i, STATE_DISALLOW);
+ }
+ mZenPolicy.mPriorityMessages = PEOPLE_TYPE_NONE;
+ mZenPolicy.mPriorityCalls = PEOPLE_TYPE_NONE;
+ mZenPolicy.mConversationSenders = CONVERSATION_SENDERS_NONE;
+ return this;
+ }
+
+ /**
+ * Allows notifications intercepted by DND to show on all surfaces when DND is active.
+ */
+ public @NonNull Builder showAllVisualEffects() {
+ for (int i = 0; i < mZenPolicy.mVisualEffects.size(); i++) {
+ mZenPolicy.mVisualEffects.set(i, STATE_ALLOW);
+ }
+ return this;
+ }
+
+ /**
+ * Disallows notifications intercepted by DND from showing when DND is active.
+ */
+ public @NonNull Builder hideAllVisualEffects() {
+ for (int i = 0; i < mZenPolicy.mVisualEffects.size(); i++) {
+ mZenPolicy.mVisualEffects.set(i, STATE_DISALLOW);
+ }
+ return this;
+ }
+
+ /**
+ * Unsets a priority category, neither allowing or disallowing. When applying this policy,
+ * unset categories will default to the current applied policy.
+ * @hide
+ */
+ public @NonNull Builder unsetPriorityCategory(@PriorityCategory int category) {
+ mZenPolicy.mPriorityCategories.set(category, STATE_UNSET);
+
+ if (category == PRIORITY_CATEGORY_MESSAGES) {
+ mZenPolicy.mPriorityMessages = STATE_UNSET;
+ } else if (category == PRIORITY_CATEGORY_CALLS) {
+ mZenPolicy.mPriorityCalls = STATE_UNSET;
+ } else if (category == PRIORITY_CATEGORY_CONVERSATIONS) {
+ mZenPolicy.mConversationSenders = STATE_UNSET;
+ }
+
+ return this;
+ }
+
+ /**
+ * Unsets a visual effect, neither allowing or disallowing. When applying this policy,
+ * unset effects will default to the current applied policy.
+ * @hide
+ */
+ public @NonNull Builder unsetVisualEffect(@VisualEffect int effect) {
+ mZenPolicy.mVisualEffects.set(effect, STATE_UNSET);
+ return this;
+ }
+
+ /**
+ * Whether to allow notifications with category {@link Notification#CATEGORY_REMINDER}
+ * to play sounds and visually appear or to intercept them when DND is active.
+ */
+ public @NonNull Builder allowReminders(boolean allow) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_REMINDERS,
+ allow ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether to allow notifications with category {@link Notification#CATEGORY_EVENT}
+ * to play sounds and visually appear or to intercept them when DND is active.
+ */
+ public @NonNull Builder allowEvents(boolean allow) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_EVENTS,
+ allow ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether to allow conversation notifications
+ * (see {@link NotificationChannel#setConversationId(String, String)})
+ * that match audienceType to play sounds and visually appear or to intercept
+ * them when DND is active.
+ * @param audienceType callers that are allowed to bypass DND
+ */
+ public @NonNull Builder allowConversations(@ConversationSenders int audienceType) {
+ if (audienceType == STATE_UNSET) {
+ return unsetPriorityCategory(PRIORITY_CATEGORY_CONVERSATIONS);
+ }
+
+ if (audienceType == CONVERSATION_SENDERS_NONE) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_CONVERSATIONS, STATE_DISALLOW);
+ } else if (audienceType == CONVERSATION_SENDERS_ANYONE
+ || audienceType == CONVERSATION_SENDERS_IMPORTANT) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_CONVERSATIONS, STATE_ALLOW);
+ } else {
+ return this;
+ }
+
+ mZenPolicy.mConversationSenders = audienceType;
+ return this;
+ }
+
+ /**
+ * Whether to allow notifications with category {@link Notification#CATEGORY_MESSAGE}
+ * that match audienceType to play sounds and visually appear or to intercept
+ * them when DND is active.
+ * @param audienceType message senders that are allowed to bypass DND
+ */
+ public @NonNull Builder allowMessages(@PeopleType int audienceType) {
+ if (audienceType == STATE_UNSET) {
+ return unsetPriorityCategory(PRIORITY_CATEGORY_MESSAGES);
+ }
+
+ if (audienceType == PEOPLE_TYPE_NONE) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_MESSAGES, STATE_DISALLOW);
+ } else if (audienceType == PEOPLE_TYPE_ANYONE || audienceType == PEOPLE_TYPE_CONTACTS
+ || audienceType == PEOPLE_TYPE_STARRED) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_MESSAGES, STATE_ALLOW);
+ } else {
+ return this;
+ }
+
+ mZenPolicy.mPriorityMessages = audienceType;
+ return this;
+ }
+
+ /**
+ * Whether to allow notifications with category {@link Notification#CATEGORY_CALL}
+ * that match audienceType to play sounds and visually appear or to intercept
+ * them when DND is active.
+ * @param audienceType callers that are allowed to bypass DND
+ */
+ public @NonNull Builder allowCalls(@PeopleType int audienceType) {
+ if (audienceType == STATE_UNSET) {
+ return unsetPriorityCategory(PRIORITY_CATEGORY_CALLS);
+ }
+
+ if (audienceType == PEOPLE_TYPE_NONE) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_CALLS, STATE_DISALLOW);
+ } else if (audienceType == PEOPLE_TYPE_ANYONE || audienceType == PEOPLE_TYPE_CONTACTS
+ || audienceType == PEOPLE_TYPE_STARRED) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_CALLS, STATE_ALLOW);
+ } else {
+ return this;
+ }
+
+ mZenPolicy.mPriorityCalls = audienceType;
+ return this;
+ }
+
+ /**
+ * Whether to allow repeat callers (notifications with category
+ * {@link Notification#CATEGORY_CALL} that have recently called
+ * to play sounds and visually appear.
+ */
+ public @NonNull Builder allowRepeatCallers(boolean allow) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_REPEAT_CALLERS,
+ allow ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether to allow notifications with category {@link Notification#CATEGORY_ALARM}
+ * to play sounds and visually appear or to intercept them when DND is active.
+ * Disallowing alarms will mute the alarm stream when DND is active.
+ */
+ public @NonNull Builder allowAlarms(boolean allow) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_ALARMS,
+ allow ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether to allow media notifications to play sounds and visually
+ * appear or to intercept them when DND is active.
+ * Disallowing media will mute the media stream when DND is active.
+ */
+ public @NonNull Builder allowMedia(boolean allow) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_MEDIA,
+ allow ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether to allow system sounds to play when DND is active.
+ * Disallowing system sounds will mute the system stream when DND is active.
+ */
+ public @NonNull Builder allowSystem(boolean allow) {
+ mZenPolicy.mPriorityCategories.set(PRIORITY_CATEGORY_SYSTEM,
+ allow ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether to allow {@link PriorityCategory} sounds to play when DND is active.
+ * @hide
+ */
+ public @NonNull Builder allowCategory(@PriorityCategory int category, boolean allow) {
+ switch (category) {
+ case PRIORITY_CATEGORY_ALARMS:
+ allowAlarms(allow);
+ break;
+ case PRIORITY_CATEGORY_MEDIA:
+ allowMedia(allow);
+ break;
+ case PRIORITY_CATEGORY_SYSTEM:
+ allowSystem(allow);
+ break;
+ case PRIORITY_CATEGORY_REMINDERS:
+ allowReminders(allow);
+ break;
+ case PRIORITY_CATEGORY_EVENTS:
+ allowEvents(allow);
+ break;
+ case PRIORITY_CATEGORY_REPEAT_CALLERS:
+ allowRepeatCallers(allow);
+ break;
+ }
+ return this;
+ }
+
+ /**
+ * Whether {@link Notification#fullScreenIntent full screen intents} that are intercepted
+ * by DND are shown.
+ */
+ public @NonNull Builder showFullScreenIntent(boolean show) {
+ mZenPolicy.mVisualEffects.set(VISUAL_EFFECT_FULL_SCREEN_INTENT,
+ show ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether {@link NotificationChannel#shouldShowLights() notification lights} from
+ * notifications intercepted by DND are blocked.
+ */
+ public @NonNull Builder showLights(boolean show) {
+ mZenPolicy.mVisualEffects.set(VISUAL_EFFECT_LIGHTS,
+ show ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether notifications intercepted by DND are prevented from peeking.
+ */
+ public @NonNull Builder showPeeking(boolean show) {
+ mZenPolicy.mVisualEffects.set(VISUAL_EFFECT_PEEK,
+ show ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether notifications intercepted by DND are prevented from appearing in the status bar
+ * on devices that support status bars.
+ */
+ public @NonNull Builder showStatusBarIcons(boolean show) {
+ mZenPolicy.mVisualEffects.set(VISUAL_EFFECT_STATUS_BAR,
+ show ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether {@link NotificationChannel#canShowBadge() badges} from
+ * notifications intercepted by DND are allowed on devices that support badging.
+ */
+ public @NonNull Builder showBadges(boolean show) {
+ mZenPolicy.mVisualEffects.set(VISUAL_EFFECT_BADGE,
+ show ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether notification intercepted by DND are prevented from appearing on ambient displays
+ * on devices that support ambient display.
+ */
+ public @NonNull Builder showInAmbientDisplay(boolean show) {
+ mZenPolicy.mVisualEffects.set(VISUAL_EFFECT_AMBIENT,
+ show ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether notification intercepted by DND are prevented from appearing in notification
+ * list views like the notification shade or lockscreen on devices that support those
+ * views.
+ */
+ public @NonNull Builder showInNotificationList(boolean show) {
+ mZenPolicy.mVisualEffects.set(VISUAL_EFFECT_NOTIFICATION_LIST,
+ show ? STATE_ALLOW : STATE_DISALLOW);
+ return this;
+ }
+
+ /**
+ * Whether notifications intercepted by DND are prevented from appearing for
+ * {@link VisualEffect}
+ * @hide
+ */
+ public @NonNull Builder showVisualEffect(@VisualEffect int effect, boolean show) {
+ switch (effect) {
+ case VISUAL_EFFECT_FULL_SCREEN_INTENT:
+ showFullScreenIntent(show);
+ break;
+ case VISUAL_EFFECT_LIGHTS:
+ showLights(show);
+ break;
+ case VISUAL_EFFECT_PEEK:
+ showPeeking(show);
+ break;
+ case VISUAL_EFFECT_STATUS_BAR:
+ showStatusBarIcons(show);
+ break;
+ case VISUAL_EFFECT_BADGE:
+ showBadges(show);
+ break;
+ case VISUAL_EFFECT_AMBIENT:
+ showInAmbientDisplay(show);
+ break;
+ case VISUAL_EFFECT_NOTIFICATION_LIST:
+ showInNotificationList(show);
+ break;
+ }
+ return this;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeList(mPriorityCategories);
+ dest.writeList(mVisualEffects);
+ dest.writeInt(mPriorityCalls);
+ dest.writeInt(mPriorityMessages);
+ dest.writeInt(mConversationSenders);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ZenPolicy> CREATOR =
+ new Parcelable.Creator<ZenPolicy>() {
+ @Override
+ public ZenPolicy createFromParcel(Parcel source) {
+ ZenPolicy policy = new ZenPolicy();
+ policy.mPriorityCategories = source.readArrayList(Integer.class.getClassLoader());
+ policy.mVisualEffects = source.readArrayList(Integer.class.getClassLoader());
+ policy.mPriorityCalls = source.readInt();
+ policy.mPriorityMessages = source.readInt();
+ policy.mConversationSenders = source.readInt();
+ return policy;
+ }
+
+ @Override
+ public ZenPolicy[] newArray(int size) {
+ return new ZenPolicy[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return new StringBuilder(ZenPolicy.class.getSimpleName())
+ .append('{')
+ .append("priorityCategories=[").append(priorityCategoriesToString())
+ .append("], visualEffects=[").append(visualEffectsToString())
+ .append("], priorityCallsSenders=").append(peopleTypeToString(mPriorityCalls))
+ .append(", priorityMessagesSenders=").append(peopleTypeToString(mPriorityMessages))
+ .append(", priorityConversationSenders=").append(
+ conversationTypeToString(mConversationSenders))
+ .append('}')
+ .toString();
+ }
+
+
+ private String priorityCategoriesToString() {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < mPriorityCategories.size(); i++) {
+ if (mPriorityCategories.get(i) != STATE_UNSET) {
+ builder.append(indexToCategory(i))
+ .append("=")
+ .append(stateToString(mPriorityCategories.get(i)))
+ .append(" ");
+ }
+
+ }
+ return builder.toString();
+ }
+
+ private String visualEffectsToString() {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < mVisualEffects.size(); i++) {
+ if (mVisualEffects.get(i) != STATE_UNSET) {
+ builder.append(indexToVisualEffect(i))
+ .append("=")
+ .append(stateToString(mVisualEffects.get(i)))
+ .append(" ");
+ }
+
+ }
+ return builder.toString();
+ }
+
+ private String indexToVisualEffect(@VisualEffect int visualEffectIndex) {
+ switch (visualEffectIndex) {
+ case VISUAL_EFFECT_FULL_SCREEN_INTENT:
+ return "fullScreenIntent";
+ case VISUAL_EFFECT_LIGHTS:
+ return "lights";
+ case VISUAL_EFFECT_PEEK:
+ return "peek";
+ case VISUAL_EFFECT_STATUS_BAR:
+ return "statusBar";
+ case VISUAL_EFFECT_BADGE:
+ return "badge";
+ case VISUAL_EFFECT_AMBIENT:
+ return "ambient";
+ case VISUAL_EFFECT_NOTIFICATION_LIST:
+ return "notificationList";
+ }
+ return null;
+ }
+
+ private String indexToCategory(@PriorityCategory int categoryIndex) {
+ switch (categoryIndex) {
+ case PRIORITY_CATEGORY_REMINDERS:
+ return "reminders";
+ case PRIORITY_CATEGORY_EVENTS:
+ return "events";
+ case PRIORITY_CATEGORY_MESSAGES:
+ return "messages";
+ case PRIORITY_CATEGORY_CALLS:
+ return "calls";
+ case PRIORITY_CATEGORY_REPEAT_CALLERS:
+ return "repeatCallers";
+ case PRIORITY_CATEGORY_ALARMS:
+ return "alarms";
+ case PRIORITY_CATEGORY_MEDIA:
+ return "media";
+ case PRIORITY_CATEGORY_SYSTEM:
+ return "system";
+ case PRIORITY_CATEGORY_CONVERSATIONS:
+ return "convs";
+ }
+ return null;
+ }
+
+ private String stateToString(@State int state) {
+ switch (state) {
+ case STATE_UNSET:
+ return "unset";
+ case STATE_DISALLOW:
+ return "disallow";
+ case STATE_ALLOW:
+ return "allow";
+ }
+ return "invalidState{" + state + "}";
+ }
+
+ private String peopleTypeToString(@PeopleType int peopleType) {
+ switch (peopleType) {
+ case PEOPLE_TYPE_ANYONE:
+ return "anyone";
+ case PEOPLE_TYPE_CONTACTS:
+ return "contacts";
+ case PEOPLE_TYPE_NONE:
+ return "none";
+ case PEOPLE_TYPE_STARRED:
+ return "starred_contacts";
+ case STATE_UNSET:
+ return "unset";
+ }
+ return "invalidPeopleType{" + peopleType + "}";
+ }
+
+ /**
+ * @hide
+ */
+ public static String conversationTypeToString(@ConversationSenders int conversationType) {
+ switch (conversationType) {
+ case CONVERSATION_SENDERS_ANYONE:
+ return "anyone";
+ case CONVERSATION_SENDERS_IMPORTANT:
+ return "important";
+ case CONVERSATION_SENDERS_NONE:
+ return "none";
+ case CONVERSATION_SENDERS_UNSET:
+ return "unset";
+ }
+ return "invalidConversationType{" + conversationType + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ZenPolicy)) return false;
+ if (o == this) return true;
+ final ZenPolicy other = (ZenPolicy) o;
+
+ return Objects.equals(other.mPriorityCategories, mPriorityCategories)
+ && Objects.equals(other.mVisualEffects, mVisualEffects)
+ && other.mPriorityCalls == mPriorityCalls
+ && other.mPriorityMessages == mPriorityMessages
+ && other.mConversationSenders == mConversationSenders;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, mPriorityMessages,
+ mConversationSenders);
+ }
+
+ private @ZenPolicy.State int getZenPolicyPriorityCategoryState(@PriorityCategory int
+ category) {
+ switch (category) {
+ case PRIORITY_CATEGORY_REMINDERS:
+ return getPriorityCategoryReminders();
+ case PRIORITY_CATEGORY_EVENTS:
+ return getPriorityCategoryEvents();
+ case PRIORITY_CATEGORY_MESSAGES:
+ return getPriorityCategoryMessages();
+ case PRIORITY_CATEGORY_CALLS:
+ return getPriorityCategoryCalls();
+ case PRIORITY_CATEGORY_REPEAT_CALLERS:
+ return getPriorityCategoryRepeatCallers();
+ case PRIORITY_CATEGORY_ALARMS:
+ return getPriorityCategoryAlarms();
+ case PRIORITY_CATEGORY_MEDIA:
+ return getPriorityCategoryMedia();
+ case PRIORITY_CATEGORY_SYSTEM:
+ return getPriorityCategorySystem();
+ case PRIORITY_CATEGORY_CONVERSATIONS:
+ return getPriorityCategoryConversations();
+ }
+ return -1;
+ }
+
+ private @ZenPolicy.State int getZenPolicyVisualEffectState(@VisualEffect int effect) {
+ switch (effect) {
+ case VISUAL_EFFECT_FULL_SCREEN_INTENT:
+ return getVisualEffectFullScreenIntent();
+ case VISUAL_EFFECT_LIGHTS:
+ return getVisualEffectLights();
+ case VISUAL_EFFECT_PEEK:
+ return getVisualEffectPeek();
+ case VISUAL_EFFECT_STATUS_BAR:
+ return getVisualEffectStatusBar();
+ case VISUAL_EFFECT_BADGE:
+ return getVisualEffectBadge();
+ case VISUAL_EFFECT_AMBIENT:
+ return getVisualEffectAmbient();
+ case VISUAL_EFFECT_NOTIFICATION_LIST:
+ return getVisualEffectNotificationList();
+ }
+ return -1;
+ }
+
+ /** @hide */
+ public boolean isCategoryAllowed(@PriorityCategory int category, boolean defaultVal) {
+ switch (getZenPolicyPriorityCategoryState(category)) {
+ case ZenPolicy.STATE_ALLOW:
+ return true;
+ case ZenPolicy.STATE_DISALLOW:
+ return false;
+ default:
+ return defaultVal;
+ }
+ }
+
+ /** @hide */
+ public boolean isVisualEffectAllowed(@VisualEffect int effect, boolean defaultVal) {
+ switch (getZenPolicyVisualEffectState(effect)) {
+ case ZenPolicy.STATE_ALLOW:
+ return true;
+ case ZenPolicy.STATE_DISALLOW:
+ return false;
+ default:
+ return defaultVal;
+ }
+ }
+
+ /**
+ * Applies another policy on top of this policy
+ * @hide
+ */
+ public void apply(ZenPolicy policyToApply) {
+ if (policyToApply == null) {
+ return;
+ }
+
+ // apply priority categories
+ for (int category = 0; category < mPriorityCategories.size(); category++) {
+ if (mPriorityCategories.get(category) == STATE_DISALLOW) {
+ // if a priority category is already disallowed by the policy, cannot allow
+ continue;
+ }
+
+ @State int newState = policyToApply.mPriorityCategories.get(category);
+ if (newState != STATE_UNSET) {
+ mPriorityCategories.set(category, newState);
+
+ if (category == PRIORITY_CATEGORY_MESSAGES
+ && mPriorityMessages < policyToApply.mPriorityMessages) {
+ mPriorityMessages = policyToApply.mPriorityMessages;
+ } else if (category == PRIORITY_CATEGORY_CALLS
+ && mPriorityCalls < policyToApply.mPriorityCalls) {
+ mPriorityCalls = policyToApply.mPriorityCalls;
+ } else if (category == PRIORITY_CATEGORY_CONVERSATIONS
+ && mConversationSenders < policyToApply.mConversationSenders) {
+ mConversationSenders = policyToApply.mConversationSenders;
+ }
+ }
+ }
+
+ // apply visual effects
+ for (int visualEffect = 0; visualEffect < mVisualEffects.size(); visualEffect++) {
+ if (mVisualEffects.get(visualEffect) == STATE_DISALLOW) {
+ // if a visual effect is already disallowed by the policy, cannot allow
+ continue;
+ }
+
+ if (policyToApply.mVisualEffects.get(visualEffect) != STATE_UNSET) {
+ mVisualEffects.set(visualEffect, policyToApply.mVisualEffects.get(visualEffect));
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(ZenPolicyProto.REMINDERS, getPriorityCategoryReminders());
+ proto.write(ZenPolicyProto.EVENTS, getPriorityCategoryEvents());
+ proto.write(ZenPolicyProto.MESSAGES, getPriorityCategoryMessages());
+ proto.write(ZenPolicyProto.CALLS, getPriorityCategoryCalls());
+ proto.write(ZenPolicyProto.REPEAT_CALLERS, getPriorityCategoryRepeatCallers());
+ proto.write(ZenPolicyProto.ALARMS, getPriorityCategoryAlarms());
+ proto.write(ZenPolicyProto.MEDIA, getPriorityCategoryMedia());
+ proto.write(ZenPolicyProto.SYSTEM, getPriorityCategorySystem());
+
+ proto.write(ZenPolicyProto.FULL_SCREEN_INTENT, getVisualEffectFullScreenIntent());
+ proto.write(ZenPolicyProto.LIGHTS, getVisualEffectLights());
+ proto.write(ZenPolicyProto.PEEK, getVisualEffectPeek());
+ proto.write(ZenPolicyProto.STATUS_BAR, getVisualEffectStatusBar());
+ proto.write(ZenPolicyProto.BADGE, getVisualEffectBadge());
+ proto.write(ZenPolicyProto.AMBIENT, getVisualEffectAmbient());
+ proto.write(ZenPolicyProto.NOTIFICATION_LIST, getVisualEffectNotificationList());
+
+ proto.write(ZenPolicyProto.PRIORITY_MESSAGES, getPriorityMessageSenders());
+ proto.write(ZenPolicyProto.PRIORITY_CALLS, getPriorityCallSenders());
+ proto.end(token);
+ }
+
+ /**
+ * Makes deep copy of this ZenPolicy.
+ * @hide
+ */
+ public @NonNull ZenPolicy copy() {
+ final Parcel parcel = Parcel.obtain();
+ try {
+ writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return CREATOR.createFromParcel(parcel);
+ } finally {
+ parcel.recycle();
+ }
+ }
+}
diff --git a/android/service/oemlock/OemLockManager.java b/android/service/oemlock/OemLockManager.java
new file mode 100644
index 0000000..029d645
--- /dev/null
+++ b/android/service/oemlock/OemLockManager.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017 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.oemlock;
+
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+
+/**
+ * Interface for managing the OEM lock on the device.
+ *
+ * This will only be available if the device implements OEM lock protection.
+ *
+ * Multiple actors have an opinion on whether the device can be OEM unlocked and they must all be in
+ * agreement for unlock to be possible.
+ *
+ * @hide
+ */
+@SystemApi
+@SystemService(Context.OEM_LOCK_SERVICE)
+public class OemLockManager {
+ private IOemLockService mService;
+
+ /** @hide */
+ public OemLockManager(IOemLockService service) {
+ mService = service;
+ }
+
+ /**
+ * Returns a vendor specific name for the OEM lock.
+ *
+ * This value is used to identify the security protocol used by locks.
+ *
+ * @return The name of the OEM lock or {@code null} if failed to get the name.
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_CARRIER_OEM_UNLOCK_STATE)
+ @Nullable
+ public String getLockName() {
+ try {
+ return mService.getLockName();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets whether the carrier has allowed this device to be OEM unlocked.
+ *
+ * Depending on the implementation, the validity of the request might need to be proved. This
+ * can be acheived by passing a signature that the system will use to verify the request is
+ * legitimate.
+ *
+ * All actors involved must agree for OEM unlock to be possible.
+ *
+ * @param allowed Whether the device should be allowed to be unlocked.
+ * @param signature Optional proof of request validity, {@code null} for none.
+ * @throws IllegalArgumentException if a signature is required but was not provided.
+ * @throws SecurityException if the wrong signature was provided.
+ *
+ * @see #isOemUnlockAllowedByCarrier()
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_CARRIER_OEM_UNLOCK_STATE)
+ public void setOemUnlockAllowedByCarrier(boolean allowed, @Nullable byte[] signature) {
+ try {
+ mService.setOemUnlockAllowedByCarrier(allowed, signature);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether the carrier has allowed this device to be OEM unlocked.
+ * @return Whether OEM unlock is allowed by the carrier, or true if no OEM lock is present.
+ *
+ * @see #setOemUnlockAllowedByCarrier(boolean, byte[])
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_CARRIER_OEM_UNLOCK_STATE)
+ public boolean isOemUnlockAllowedByCarrier() {
+ try {
+ return mService.isOemUnlockAllowedByCarrier();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets whether the user has allowed this device to be unlocked.
+ *
+ * All actors involved must agree for OEM unlock to be possible.
+ *
+ * @param allowed Whether the device should be allowed to be unlocked.
+ * @throws SecurityException if the user is not allowed to unlock the device.
+ *
+ * @see #isOemUnlockAllowedByUser()
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_USER_OEM_UNLOCK_STATE)
+ public void setOemUnlockAllowedByUser(boolean allowed) {
+ try {
+ mService.setOemUnlockAllowedByUser(allowed);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether, or not, the user has allowed this device to be OEM unlocked.
+ * @return Whether OEM unlock is allowed by the user, or true if no OEM lock is present.
+ *
+ * @see #setOemUnlockAllowedByUser(boolean)
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_USER_OEM_UNLOCK_STATE)
+ public boolean isOemUnlockAllowedByUser() {
+ try {
+ return mService.isOemUnlockAllowedByUser();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @return Whether the bootloader is able to OEM unlock the device.
+ *
+ * @hide
+ */
+ public boolean isOemUnlockAllowed() {
+ try {
+ return mService.isOemUnlockAllowed();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @return Whether the device has been OEM unlocked by the bootloader.
+ *
+ * @hide
+ */
+ public boolean isDeviceOemUnlocked() {
+ try {
+ return mService.isDeviceOemUnlocked();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/service/persistentdata/PersistentDataBlockManager.java b/android/service/persistentdata/PersistentDataBlockManager.java
new file mode 100644
index 0000000..0bf68b7
--- /dev/null
+++ b/android/service/persistentdata/PersistentDataBlockManager.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2014 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.persistentdata;
+
+import android.annotation.IntDef;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+import android.service.oemlock.OemLockManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Interface for reading and writing data blocks to a persistent partition.
+ *
+ * Allows writing one block at a time. Namely, each time
+ * {@link PersistentDataBlockManager#write(byte[])}
+ * is called, it will overwite the data that was previously written on the block.
+ *
+ * Clients can query the size of the currently written block via
+ * {@link PersistentDataBlockManager#getDataBlockSize()}.
+ *
+ * Clients can query the maximum size for a block via
+ * {@link PersistentDataBlockManager#getMaximumDataBlockSize()}
+ *
+ * Clients can read the currently written block by invoking
+ * {@link PersistentDataBlockManager#read()}.
+ *
+ * @hide
+ */
+@SystemApi
+@SystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE)
+public class PersistentDataBlockManager {
+ private static final String TAG = PersistentDataBlockManager.class.getSimpleName();
+ private IPersistentDataBlockService sService;
+
+ /**
+ * Indicates that the device's bootloader lock state is UNKNOWN.
+ */
+ public static final int FLASH_LOCK_UNKNOWN = -1;
+ /**
+ * Indicates that the device's bootloader is UNLOCKED.
+ */
+ public static final int FLASH_LOCK_UNLOCKED = 0;
+ /**
+ * Indicates that the device's bootloader is LOCKED.
+ */
+ public static final int FLASH_LOCK_LOCKED = 1;
+
+ @IntDef(prefix = { "FLASH_LOCK_" }, value = {
+ FLASH_LOCK_UNKNOWN,
+ FLASH_LOCK_LOCKED,
+ FLASH_LOCK_UNLOCKED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FlashLockState {}
+
+ /** @hide */
+ public PersistentDataBlockManager(IPersistentDataBlockService service) {
+ sService = service;
+ }
+
+ /**
+ * Writes {@code data} to the persistent partition. Previously written data
+ * will be overwritten. This data will persist across factory resets.
+ *
+ * Returns the number of bytes written or -1 on error. If the block is too big
+ * to fit on the partition, returns -MAX_BLOCK_SIZE.
+ *
+ * {@link #wipe} will block any further {@link #write} operation until reboot,
+ * in which case -1 will be returned.
+ *
+ * @param data the data to write
+ */
+ @SuppressLint("Doclava125")
+ public int write(byte[] data) {
+ try {
+ return sService.write(data);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the data block stored on the persistent partition.
+ */
+ @SuppressLint("Doclava125")
+ public byte[] read() {
+ try {
+ return sService.read();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves the size of the block currently written to the persistent partition.
+ *
+ * Return -1 on error.
+ */
+ @RequiresPermission(android.Manifest.permission.ACCESS_PDB_STATE)
+ public int getDataBlockSize() {
+ try {
+ return sService.getDataBlockSize();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves the maximum size allowed for a data block.
+ *
+ * Returns -1 on error.
+ */
+ @SuppressLint("Doclava125")
+ public long getMaximumDataBlockSize() {
+ try {
+ return sService.getMaximumDataBlockSize();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Zeroes the previously written block in its entirety. Calling this method
+ * will erase all data written to the persistent data partition.
+ * It will also prevent any further {@link #write} operation until reboot,
+ * in order to prevent a potential race condition. See b/30352311.
+ */
+ @RequiresPermission(android.Manifest.permission.OEM_UNLOCK_STATE)
+ public void wipe() {
+ try {
+ sService.wipe();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Writes a byte enabling or disabling the ability to "OEM unlock" the device.
+ *
+ * @deprecated use {@link OemLockManager#setOemUnlockAllowedByUser(boolean)} instead.
+ */
+ @RequiresPermission(android.Manifest.permission.OEM_UNLOCK_STATE)
+ public void setOemUnlockEnabled(boolean enabled) {
+ try {
+ sService.setOemUnlockEnabled(enabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether or not "OEM unlock" is enabled or disabled on this device.
+ *
+ * @deprecated use {@link OemLockManager#isOemUnlockAllowedByUser()} instead.
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.READ_OEM_UNLOCK_STATE,
+ android.Manifest.permission.OEM_UNLOCK_STATE
+ })
+ public boolean getOemUnlockEnabled() {
+ try {
+ return sService.getOemUnlockEnabled();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves available information about this device's flash lock state.
+ *
+ * @return {@link #FLASH_LOCK_LOCKED} if device bootloader is locked,
+ * {@link #FLASH_LOCK_UNLOCKED} if device bootloader is unlocked, or {@link #FLASH_LOCK_UNKNOWN}
+ * if this information cannot be ascertained on this device.
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.READ_OEM_UNLOCK_STATE,
+ android.Manifest.permission.OEM_UNLOCK_STATE
+ })
+ @FlashLockState
+ public int getFlashLockState() {
+ try {
+ return sService.getFlashLockState();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/service/quickaccesswallet/GetWalletCardsCallback.java b/android/service/quickaccesswallet/GetWalletCardsCallback.java
new file mode 100644
index 0000000..f6a86d9
--- /dev/null
+++ b/android/service/quickaccesswallet/GetWalletCardsCallback.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 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.quickaccesswallet;
+
+import android.annotation.NonNull;
+
+/**
+ * Handles response from the {@link QuickAccessWalletService} for {@link GetWalletCardsRequest}
+ */
+public interface GetWalletCardsCallback {
+
+ /**
+ * Notifies the Android System that an {@link QuickAccessWalletService#onWalletCardsRequested}
+ * was successfully handled by the service.
+ *
+ * @param response The response contains the list of {@link WalletCard walletCards} to be shown
+ * to the user as well as the index of the card that should initially be
+ * presented as the selected card. The list should not contain more than the
+ * maximum number of cards requested.
+ */
+ void onSuccess(@NonNull GetWalletCardsResponse response);
+
+ /**
+ * Notifies the Android System that an {@link QuickAccessWalletService#onWalletCardsRequested}
+ * could not be handled by the service.
+ *
+ * @param error The error message. <b>Note: </b> this message should <b>not</b> contain PII
+ * (Personally Identifiable Information, such as username or email address).
+ * @throws IllegalStateException if this method or {@link #onSuccess} was already called.
+ */
+ void onFailure(@NonNull GetWalletCardsError error);
+}
diff --git a/android/service/quickaccesswallet/GetWalletCardsCallbackImpl.java b/android/service/quickaccesswallet/GetWalletCardsCallbackImpl.java
new file mode 100644
index 0000000..ae67068
--- /dev/null
+++ b/android/service/quickaccesswallet/GetWalletCardsCallbackImpl.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 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.quickaccesswallet;
+
+import android.annotation.NonNull;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Handles response from the {@link QuickAccessWalletService} for {@link GetWalletCardsRequest}
+ *
+ * @hide
+ */
+final class GetWalletCardsCallbackImpl implements GetWalletCardsCallback {
+
+ private static final String TAG = "QAWalletCallback";
+
+ private final IQuickAccessWalletServiceCallbacks mCallback;
+ private final GetWalletCardsRequest mRequest;
+ private final Handler mHandler;
+ private boolean mCalled;
+
+ GetWalletCardsCallbackImpl(GetWalletCardsRequest request,
+ IQuickAccessWalletServiceCallbacks callback, Handler handler) {
+ mRequest = request;
+ mCallback = callback;
+ mHandler = handler;
+ }
+
+ /**
+ * Notifies the Android System that an {@link QuickAccessWalletService#onWalletCardsRequested}
+ * was successfully handled by the service.
+ *
+ * @param response The response contains the list of {@link WalletCard walletCards} to be shown
+ * to the user as well as the index of the card that should initially be
+ * presented as the selected card.
+ */
+ public void onSuccess(@NonNull GetWalletCardsResponse response) {
+ if (isValidResponse(response)) {
+ mHandler.post(() -> onSuccessInternal(response));
+ } else {
+ Log.w(TAG, "Invalid GetWalletCards response");
+ mHandler.post(() -> onFailureInternal(new GetWalletCardsError(null, null)));
+ }
+ }
+
+ /**
+ * Notifies the Android System that an {@link QuickAccessWalletService#onWalletCardsRequested}
+ * could not be handled by the service.
+ *
+ * @param error The error message. <b>Note: </b> this message should <b>not</b> contain PII
+ * (Personally Identifiable Information, such as username or email address).
+ * @throws IllegalStateException if this method or {@link #onSuccess} was already called.
+ */
+ public void onFailure(@NonNull GetWalletCardsError error) {
+ mHandler.post(() -> onFailureInternal(error));
+ }
+
+ private void onSuccessInternal(GetWalletCardsResponse response) {
+ if (mCalled) {
+ Log.w(TAG, "already called");
+ return;
+ }
+ mCalled = true;
+ try {
+ mCallback.onGetWalletCardsSuccess(response);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error returning wallet cards", e);
+ }
+ }
+
+ private void onFailureInternal(GetWalletCardsError error) {
+ if (mCalled) {
+ Log.w(TAG, "already called");
+ return;
+ }
+ mCalled = true;
+ try {
+ mCallback.onGetWalletCardsFailure(error);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error returning failure message", e);
+ }
+ }
+
+ private boolean isValidResponse(@NonNull GetWalletCardsResponse response) {
+ if (response == null) {
+ Log.w(TAG, "Invalid response: response is null");
+ return false;
+ }
+ if (response.getWalletCards() == null) {
+ Log.w(TAG, "Invalid response: walletCards is null");
+ return false;
+ }
+ if (response.getSelectedIndex() < 0) {
+ Log.w(TAG, "Invalid response: selectedIndex is negative");
+ return false;
+ }
+ if (!response.getWalletCards().isEmpty()
+ && response.getSelectedIndex() >= response.getWalletCards().size()) {
+ Log.w(TAG, "Invalid response: selectedIndex out of bounds");
+ return false;
+ }
+ if (response.getWalletCards().size() > mRequest.getMaxCards()) {
+ Log.w(TAG, "Invalid response: too many cards");
+ return false;
+ }
+ for (WalletCard walletCard : response.getWalletCards()) {
+ if (walletCard == null) {
+ Log.w(TAG, "Invalid response: card is null");
+ return false;
+ }
+ if (walletCard.getCardId() == null) {
+ Log.w(TAG, "Invalid response: cardId is null");
+ return false;
+ }
+ Icon cardImage = walletCard.getCardImage();
+ if (cardImage == null) {
+ Log.w(TAG, "Invalid response: cardImage is null");
+ return false;
+ }
+ if (cardImage.getType() == Icon.TYPE_BITMAP
+ && cardImage.getBitmap().getConfig() != Bitmap.Config.HARDWARE) {
+ Log.w(TAG, "Invalid response: cardImage bitmaps must be hardware bitmaps");
+ return false;
+ }
+ if (TextUtils.isEmpty(walletCard.getContentDescription())) {
+ Log.w(TAG, "Invalid response: contentDescription is null");
+ return false;
+ }
+ if (walletCard.getPendingIntent() == null) {
+ Log.w(TAG, "Invalid response: pendingIntent is null");
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/android/service/quickaccesswallet/GetWalletCardsError.java b/android/service/quickaccesswallet/GetWalletCardsError.java
new file mode 100644
index 0000000..527d2b7
--- /dev/null
+++ b/android/service/quickaccesswallet/GetWalletCardsError.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2020 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.quickaccesswallet;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * Error response for an {@link GetWalletCardsRequest}.
+ */
+public final class GetWalletCardsError implements Parcelable {
+
+ private final Icon mIcon;
+ private final CharSequence mMessage;
+
+ /**
+ * Construct a new error response. If provided, the icon and message will be displayed to the
+ * user.
+ *
+ * @param icon an icon to be shown to the user next to the message. Optional.
+ * @param message message to be shown to the user. Optional.
+ */
+ public GetWalletCardsError(@Nullable Icon icon, @Nullable CharSequence message) {
+ mIcon = icon;
+ mMessage = message;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ if (mIcon == null) {
+ dest.writeByte((byte) 0);
+ } else {
+ dest.writeByte((byte) 1);
+ mIcon.writeToParcel(dest, flags);
+ }
+ TextUtils.writeToParcel(mMessage, dest, flags);
+ }
+
+ private static GetWalletCardsError readFromParcel(Parcel source) {
+ Icon icon = source.readByte() == 0 ? null : Icon.CREATOR.createFromParcel(source);
+ CharSequence message = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ return new GetWalletCardsError(icon, message);
+ }
+
+ @NonNull
+ public static final Creator<GetWalletCardsError> CREATOR =
+ new Creator<GetWalletCardsError>() {
+ @Override
+ public GetWalletCardsError createFromParcel(Parcel source) {
+ return readFromParcel(source);
+ }
+
+ @Override
+ public GetWalletCardsError[] newArray(int size) {
+ return new GetWalletCardsError[size];
+ }
+ };
+
+ /**
+ * An icon that may be displayed with the message to provide a visual indication of why cards
+ * could not be provided in the Quick Access Wallet.
+ */
+ @Nullable
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * A localized message that may be shown to the user in the event that the wallet cards cannot
+ * be retrieved. <b>Note: </b> this message should <b>not</b> contain PII (Personally
+ * Identifiable Information, such as username or email address).
+ */
+ @Nullable
+ public CharSequence getMessage() {
+ return mMessage;
+ }
+}
diff --git a/android/service/quickaccesswallet/GetWalletCardsRequest.java b/android/service/quickaccesswallet/GetWalletCardsRequest.java
new file mode 100644
index 0000000..2ba448f
--- /dev/null
+++ b/android/service/quickaccesswallet/GetWalletCardsRequest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2020 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.quickaccesswallet;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents a request to a {@link QuickAccessWalletService} for {@link WalletCard walletCards}.
+ * Wallet cards may represent anything that a user might carry in their wallet -- a credit card,
+ * library card, a transit pass, etc. This request contains the desired size of the card images and
+ * icons as well as the maximum number of cards that may be returned in the {@link
+ * GetWalletCardsResponse}.
+ *
+ * <p>Cards may be displayed with an optional icon and label. The icon and label should communicate
+ * the same idea. For example, if a card can be used at an NFC terminal, the icon could be an NFC
+ * icon and the label could inform the user how to interact with the NFC terminal.
+ *
+ * <p>The maximum number of cards that may be displayed in the wallet is provided in {@link
+ * #getMaxCards()}. The {@link QuickAccessWalletService} may provide up to this many cards in the
+ * {@link GetWalletCardsResponse#getWalletCards()}. If the list of cards provided exceeds this
+ * number, some of the cards may not be shown to the user.
+ */
+public final class GetWalletCardsRequest implements Parcelable {
+
+ private final int mCardWidthPx;
+ private final int mCardHeightPx;
+ private final int mIconSizePx;
+ private final int mMaxCards;
+
+ /**
+ * Creates a new GetWalletCardsRequest.
+ *
+ * @param cardWidthPx The width of the card image in pixels.
+ * @param cardHeightPx The height of the card image in pixels.
+ * @param iconSizePx The width and height of the optional card icon in pixels.
+ * @param maxCards The maximum number of cards that may be provided in the response.
+ */
+ public GetWalletCardsRequest(int cardWidthPx, int cardHeightPx, int iconSizePx, int maxCards) {
+ this.mCardWidthPx = cardWidthPx;
+ this.mCardHeightPx = cardHeightPx;
+ this.mIconSizePx = iconSizePx;
+ this.mMaxCards = maxCards;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mCardWidthPx);
+ dest.writeInt(mCardHeightPx);
+ dest.writeInt(mIconSizePx);
+ dest.writeInt(mMaxCards);
+ }
+
+ @NonNull
+ public static final Creator<GetWalletCardsRequest> CREATOR =
+ new Creator<GetWalletCardsRequest>() {
+ @Override
+ public GetWalletCardsRequest createFromParcel(Parcel source) {
+ int cardWidthPx = source.readInt();
+ int cardHeightPx = source.readInt();
+ int iconSizePx = source.readInt();
+ int maxCards = source.readInt();
+ return new GetWalletCardsRequest(cardWidthPx,
+ cardHeightPx,
+ iconSizePx,
+ maxCards);
+ }
+
+ @Override
+ public GetWalletCardsRequest[] newArray(int size) {
+ return new GetWalletCardsRequest[size];
+ }
+ };
+
+ /**
+ * The desired width of the {@link WalletCard#getCardImage()}, in pixels. The dimensions of the
+ * card image are requested so that it may be rendered without scaling.
+ * <p>
+ * The {@code cardWidthPx} and {@code cardHeightPx} should be applied to the size of the {@link
+ * WalletCard#getCardImage()}. The size of the card image is specified so that it may be
+ * rendered accurately and without distortion caused by scaling.
+ */
+ public int getCardWidthPx() {
+ return mCardWidthPx;
+ }
+
+ /**
+ * The desired height of the {@link WalletCard#getCardImage()}, in pixels. The dimensions of the
+ * card image are requested so that it may be rendered without scaling.
+ */
+ public int getCardHeightPx() {
+ return mCardHeightPx;
+ }
+
+ /**
+ * Wallet cards may be displayed next to an icon. The icon can help to convey additional
+ * information about the state of the card. If the provided icon is a bitmap, its width and
+ * height should equal iconSizePx so that it is rendered without distortion caused by scaling.
+ */
+ public int getIconSizePx() {
+ return mIconSizePx;
+ }
+
+ /**
+ * The maximum size of the {@link GetWalletCardsResponse#getWalletCards()}. If the list of cards
+ * exceeds this number, not all cards may be displayed.
+ */
+ public int getMaxCards() {
+ return mMaxCards;
+ }
+}
diff --git a/android/service/quickaccesswallet/GetWalletCardsResponse.java b/android/service/quickaccesswallet/GetWalletCardsResponse.java
new file mode 100644
index 0000000..0551e27
--- /dev/null
+++ b/android/service/quickaccesswallet/GetWalletCardsResponse.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2020 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.quickaccesswallet;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The response for an {@link GetWalletCardsRequest} contains a list of wallet cards and the index
+ * of the card that should initially be displayed in the 'selected' position.
+ */
+public final class GetWalletCardsResponse implements Parcelable {
+
+ private final List<WalletCard> mWalletCards;
+ private final int mSelectedIndex;
+
+ /**
+ * Construct a new response.
+ *
+ * @param walletCards The list of wallet cards. The list may be empty but must NOT be larger
+ * than {@link GetWalletCardsRequest#getMaxCards()}. The list may not
+ * contain null values.
+ * @param selectedIndex The index of the card that should be presented as the initially
+ * 'selected' card. The index must be greater than or equal to zero and
+ * less than the size of the list of walletCards (unless the list is empty
+ * in which case the value may be 0).
+ */
+ public GetWalletCardsResponse(@NonNull List<WalletCard> walletCards, int selectedIndex) {
+ this.mWalletCards = walletCards;
+ this.mSelectedIndex = selectedIndex;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mWalletCards.size());
+ dest.writeParcelableList(mWalletCards, flags);
+ dest.writeInt(mSelectedIndex);
+ }
+
+ private static GetWalletCardsResponse readFromParcel(Parcel source) {
+ int size = source.readInt();
+ List<WalletCard> walletCards =
+ source.readParcelableList(new ArrayList<>(size), WalletCard.class.getClassLoader());
+ int selectedIndex = source.readInt();
+ return new GetWalletCardsResponse(walletCards, selectedIndex);
+ }
+
+ @NonNull
+ public static final Creator<GetWalletCardsResponse> CREATOR =
+ new Creator<GetWalletCardsResponse>() {
+ @Override
+ public GetWalletCardsResponse createFromParcel(Parcel source) {
+ return readFromParcel(source);
+ }
+
+ @Override
+ public GetWalletCardsResponse[] newArray(int size) {
+ return new GetWalletCardsResponse[size];
+ }
+ };
+
+ /**
+ * The list of {@link WalletCard}s. The size of this list should not exceed {@link
+ * GetWalletCardsRequest#getMaxCards()}.
+ */
+ @NonNull
+ public List<WalletCard> getWalletCards() {
+ return mWalletCards;
+ }
+
+ /**
+ * The {@code selectedIndex} represents the index of the card that should be presented in the
+ * 'selected' position when the cards are initially displayed in the quick access wallet. The
+ * {@code selectedIndex} should be greater than or equal to zero and less than the size of the
+ * list of {@link WalletCard walletCards}, unless the list is empty in which case the {@code
+ * selectedIndex} can take any value. 0 is a nice round number for such cases.
+ */
+ public int getSelectedIndex() {
+ return mSelectedIndex;
+ }
+}
diff --git a/android/service/quickaccesswallet/QuickAccessWalletClient.java b/android/service/quickaccesswallet/QuickAccessWalletClient.java
new file mode 100644
index 0000000..f122561
--- /dev/null
+++ b/android/service/quickaccesswallet/QuickAccessWalletClient.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2020 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.quickaccesswallet;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+
+import java.io.Closeable;
+import java.util.concurrent.Executor;
+
+/**
+ * Facilitates accessing cards from the {@link QuickAccessWalletService}.
+ *
+ * @hide
+ */
+@TestApi
+public interface QuickAccessWalletClient extends Closeable {
+
+ /**
+ * Create a client for accessing wallet cards from the {@link QuickAccessWalletService}. If the
+ * service is unavailable, {@link #isWalletServiceAvailable()} will return false.
+ */
+ @NonNull
+ static QuickAccessWalletClient create(@NonNull Context context) {
+ return new QuickAccessWalletClientImpl(context);
+ }
+
+ /**
+ * @return true if the {@link QuickAccessWalletService} is available. This means that the
+ * default NFC payment application has an exported service that can provide cards to the Quick
+ * Access Wallet. However, it does not mean that (1) the call will necessarily be successful,
+ * nor does it mean that cards may be displayed at this time. Addition checks are required:
+ * <ul>
+ * <li>If {@link #isWalletFeatureAvailable()} is false, cards should not be displayed
+ * <li>If the device is locked and {@link #isWalletFeatureAvailableWhenDeviceLocked} is
+ * false, cards should not be displayed while the device remains locked. (A message
+ * prompting the user to unlock to view cards may be appropriate).</li>
+ * </ul>
+ */
+ boolean isWalletServiceAvailable();
+
+ /**
+ * Wallet cards should not be displayed if:
+ * <ul>
+ * <li>The wallet service is unavailable</li>
+ * <li>The device is not provisioned, ie user setup is incomplete</li>
+ * <li>If the wallet feature has been disabled by the user</li>
+ * <li>If the phone has been put into lockdown mode</li>
+ * </ul>
+ * <p>
+ * Quick Access Wallet implementers should call this method before calling
+ * {@link #getWalletCards} to ensure that cards may be displayed.
+ */
+ boolean isWalletFeatureAvailable();
+
+ /**
+ * Wallet cards may not be displayed on the lock screen if the user has opted to hide
+ * notifications or sensitive content on the lock screen.
+ * <ul>
+ * <li>The device is not provisioned, ie user setup is incomplete</li>
+ * <li>If the wallet feature has been disabled by the user</li>
+ * <li>If the phone has been put into lockdown mode</li>
+ * </ul>
+ *
+ * <p>
+ * Quick Access Wallet implementers should call this method before calling
+ * {@link #getWalletCards} if the device is currently locked.
+ *
+ * @return true if cards may be displayed on the lock screen.
+ */
+ boolean isWalletFeatureAvailableWhenDeviceLocked();
+
+ /**
+ * Get wallet cards from the {@link QuickAccessWalletService}.
+ */
+ void getWalletCards(
+ @NonNull GetWalletCardsRequest request,
+ @NonNull OnWalletCardsRetrievedCallback callback);
+
+ /**
+ * Get wallet cards from the {@link QuickAccessWalletService}.
+ */
+ void getWalletCards(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull GetWalletCardsRequest request,
+ @NonNull OnWalletCardsRetrievedCallback callback);
+
+ /**
+ * Callback for getWalletCards
+ */
+ interface OnWalletCardsRetrievedCallback {
+ void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response);
+
+ void onWalletCardRetrievalError(@NonNull GetWalletCardsError error);
+ }
+
+ /**
+ * Notify the {@link QuickAccessWalletService} service that a wallet card was selected.
+ */
+ void selectWalletCard(@NonNull SelectWalletCardRequest request);
+
+ /**
+ * Notify the {@link QuickAccessWalletService} service that the Wallet was dismissed.
+ */
+ void notifyWalletDismissed();
+
+ /**
+ * Register an event listener.
+ */
+ void addWalletServiceEventListener(@NonNull WalletServiceEventListener listener);
+
+ /**
+ * Register an event listener.
+ */
+ void addWalletServiceEventListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull WalletServiceEventListener listener);
+
+ /**
+ * Unregister an event listener
+ */
+ void removeWalletServiceEventListener(@NonNull WalletServiceEventListener listener);
+
+ /**
+ * A listener for {@link WalletServiceEvent walletServiceEvents}
+ */
+ interface WalletServiceEventListener {
+ void onWalletServiceEvent(@NonNull WalletServiceEvent event);
+ }
+
+ /**
+ * Unregister all event listeners and disconnect from the service.
+ */
+ void disconnect();
+
+ /**
+ * The manifest entry for the QuickAccessWalletService may also publish information about the
+ * activity that hosts the Wallet view. This is typically the home screen of the Wallet
+ * application.
+ */
+ @Nullable
+ Intent createWalletIntent();
+
+ /**
+ * The manifest entry for the {@link QuickAccessWalletService} may publish the activity that
+ * hosts the settings
+ */
+ @Nullable
+ Intent createWalletSettingsIntent();
+
+ /**
+ * Returns the logo associated with the {@link QuickAccessWalletService}. This is specified by
+ * {@code android:logo} manifest entry. If the logo is not specified, the app icon will be
+ * returned instead ({@code android:icon}).
+ *
+ * @hide
+ */
+ @Nullable
+ Drawable getLogo();
+
+ /**
+ * Returns the service label specified by {@code android:label} in the service manifest entry.
+ *
+ * @hide
+ */
+ @Nullable
+ CharSequence getServiceLabel();
+
+ /**
+ * Returns the text specified by the {@link android:shortcutShortLabel} in the service manifest
+ * entry. If the shortcutShortLabel isn't specified, the service label ({@code android:label})
+ * will be returned instead.
+ *
+ * @hide
+ */
+ @Nullable
+ CharSequence getShortcutShortLabel();
+
+ /**
+ * Returns the text specified by the {@link android:shortcutLongLabel} in the service manifest
+ * entry. If the shortcutShortLabel isn't specified, the service label ({@code android:label})
+ * will be returned instead.
+ *
+ * @hide
+ */
+ @Nullable
+ CharSequence getShortcutLongLabel();
+}
diff --git a/android/service/quickaccesswallet/QuickAccessWalletClientImpl.java b/android/service/quickaccesswallet/QuickAccessWalletClientImpl.java
new file mode 100644
index 0000000..9d0b582
--- /dev/null
+++ b/android/service/quickaccesswallet/QuickAccessWalletClientImpl.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright 2020 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.quickaccesswallet;
+
+import static android.service.quickaccesswallet.QuickAccessWalletService.ACTION_VIEW_WALLET;
+import static android.service.quickaccesswallet.QuickAccessWalletService.ACTION_VIEW_WALLET_SETTINGS;
+import static android.service.quickaccesswallet.QuickAccessWalletService.SERVICE_INTERFACE;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+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.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.widget.LockPatternUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+
+/**
+ * Implements {@link QuickAccessWalletClient}. The client connects, performs requests, waits for
+ * responses, and disconnects automatically one minute after the last call is performed.
+ *
+ * @hide
+ */
+public class QuickAccessWalletClientImpl implements QuickAccessWalletClient, ServiceConnection {
+
+ private static final String TAG = "QAWalletSClient";
+ private final Handler mHandler;
+ private final Context mContext;
+ private final Queue<ApiCaller> mRequestQueue;
+ private final Map<WalletServiceEventListener, String> mEventListeners;
+ private boolean mIsConnected;
+ /**
+ * Timeout for active service connections (1 minute)
+ */
+ private static final long SERVICE_CONNECTION_TIMEOUT_MS = 60 * 1000;
+ @Nullable
+ private IQuickAccessWalletService mService;
+
+ @Nullable
+ private final QuickAccessWalletServiceInfo mServiceInfo;
+
+ private static final int MSG_TIMEOUT_SERVICE = 5;
+
+ QuickAccessWalletClientImpl(@NonNull Context context) {
+ mContext = context.getApplicationContext();
+ mServiceInfo = QuickAccessWalletServiceInfo.tryCreate(context);
+ mHandler = new Handler(Looper.getMainLooper());
+ mRequestQueue = new LinkedList<>();
+ mEventListeners = new HashMap<>(1);
+ }
+
+ @Override
+ public boolean isWalletServiceAvailable() {
+ return mServiceInfo != null;
+ }
+
+ @Override
+ public boolean isWalletFeatureAvailable() {
+ int currentUser = ActivityManager.getCurrentUser();
+ return currentUser == UserHandle.USER_SYSTEM
+ && checkUserSetupComplete()
+ && checkSecureSetting(Settings.Secure.GLOBAL_ACTIONS_PANEL_ENABLED)
+ && !new LockPatternUtils(mContext).isUserInLockdown(currentUser);
+ }
+
+ @Override
+ public boolean isWalletFeatureAvailableWhenDeviceLocked() {
+ return checkSecureSetting(Settings.Secure.POWER_MENU_LOCKED_SHOW_CONTENT);
+ }
+
+ @Override
+ public void getWalletCards(
+ @NonNull GetWalletCardsRequest request,
+ @NonNull OnWalletCardsRetrievedCallback callback) {
+ getWalletCards(mContext.getMainExecutor(), request, callback);
+ }
+
+ @Override
+ public void getWalletCards(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull GetWalletCardsRequest request,
+ @NonNull OnWalletCardsRetrievedCallback callback) {
+ if (!isWalletServiceAvailable()) {
+ executor.execute(
+ () -> callback.onWalletCardRetrievalError(new GetWalletCardsError(null, null)));
+ return;
+ }
+
+ BaseCallbacks serviceCallback = new BaseCallbacks() {
+ @Override
+ public void onGetWalletCardsSuccess(GetWalletCardsResponse response) {
+ executor.execute(() -> callback.onWalletCardsRetrieved(response));
+ }
+
+ @Override
+ public void onGetWalletCardsFailure(GetWalletCardsError error) {
+ executor.execute(() -> callback.onWalletCardRetrievalError(error));
+ }
+ };
+
+ executeApiCall(new ApiCaller("onWalletCardsRequested") {
+ @Override
+ public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
+ service.onWalletCardsRequested(request, serviceCallback);
+ }
+
+ @Override
+ public void onApiError() {
+ serviceCallback.onGetWalletCardsFailure(new GetWalletCardsError(null, null));
+ }
+ });
+
+ }
+
+ @Override
+ public void selectWalletCard(@NonNull SelectWalletCardRequest request) {
+ if (!isWalletServiceAvailable()) {
+ return;
+ }
+ executeApiCall(new ApiCaller("onWalletCardSelected") {
+ @Override
+ public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
+ service.onWalletCardSelected(request);
+ }
+ });
+ }
+
+ @Override
+ public void notifyWalletDismissed() {
+ if (!isWalletServiceAvailable()) {
+ return;
+ }
+ executeApiCall(new ApiCaller("onWalletDismissed") {
+ @Override
+ public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
+ service.onWalletDismissed();
+ }
+ });
+ }
+
+ @Override
+ public void addWalletServiceEventListener(WalletServiceEventListener listener) {
+ addWalletServiceEventListener(mContext.getMainExecutor(), listener);
+ }
+
+ @Override
+ public void addWalletServiceEventListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull WalletServiceEventListener listener) {
+ if (!isWalletServiceAvailable()) {
+ return;
+ }
+ BaseCallbacks callback = new BaseCallbacks() {
+ @Override
+ public void onWalletServiceEvent(WalletServiceEvent event) {
+ executor.execute(() -> listener.onWalletServiceEvent(event));
+ }
+ };
+
+ executeApiCall(new ApiCaller("registerListener") {
+ @Override
+ public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
+ String listenerId = UUID.randomUUID().toString();
+ WalletServiceEventListenerRequest request =
+ new WalletServiceEventListenerRequest(listenerId);
+ mEventListeners.put(listener, listenerId);
+ service.registerWalletServiceEventListener(request, callback);
+ }
+ });
+ }
+
+ @Override
+ public void removeWalletServiceEventListener(WalletServiceEventListener listener) {
+ if (!isWalletServiceAvailable()) {
+ return;
+ }
+ executeApiCall(new ApiCaller("unregisterListener") {
+ @Override
+ public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
+ String listenerId = mEventListeners.remove(listener);
+ if (listenerId == null) {
+ return;
+ }
+ WalletServiceEventListenerRequest request =
+ new WalletServiceEventListenerRequest(listenerId);
+ service.unregisterWalletServiceEventListener(request);
+ }
+ });
+ }
+
+ @Override
+ public void close() throws IOException {
+ disconnect();
+ }
+
+ @Override
+ public void disconnect() {
+ mHandler.post(() -> disconnectInternal(true));
+ }
+
+ @Override
+ @Nullable
+ public Intent createWalletIntent() {
+ if (mServiceInfo == null) {
+ return null;
+ }
+ String packageName = mServiceInfo.getComponentName().getPackageName();
+ String walletActivity = mServiceInfo.getWalletActivity();
+ return createIntent(walletActivity, packageName, ACTION_VIEW_WALLET);
+ }
+
+ @Override
+ @Nullable
+ public Intent createWalletSettingsIntent() {
+ if (mServiceInfo == null) {
+ return null;
+ }
+ String packageName = mServiceInfo.getComponentName().getPackageName();
+ String settingsActivity = mServiceInfo.getSettingsActivity();
+ return createIntent(settingsActivity, packageName, ACTION_VIEW_WALLET_SETTINGS);
+ }
+
+ @Nullable
+ private Intent createIntent(@Nullable String activityName, String packageName, String action) {
+ PackageManager pm = mContext.getPackageManager();
+ if (TextUtils.isEmpty(activityName)) {
+ activityName = queryActivityForAction(pm, packageName, action);
+ }
+ if (TextUtils.isEmpty(activityName)) {
+ return null;
+ }
+ ComponentName component = new ComponentName(packageName, activityName);
+ if (!isActivityEnabled(pm, component)) {
+ return null;
+ }
+ return new Intent(action).setComponent(component);
+ }
+
+ @Nullable
+ private static String queryActivityForAction(PackageManager pm, String packageName,
+ String action) {
+ Intent intent = new Intent(action).setPackage(packageName);
+ ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
+ if (resolveInfo == null
+ || resolveInfo.activityInfo == null
+ || !resolveInfo.activityInfo.exported) {
+ return null;
+ }
+ return resolveInfo.activityInfo.name;
+ }
+
+ private static boolean isActivityEnabled(PackageManager pm, ComponentName component) {
+ int setting = pm.getComponentEnabledSetting(component);
+ if (setting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
+ return true;
+ }
+ if (setting != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
+ return false;
+ }
+ try {
+ return pm.getActivityInfo(component, 0).isEnabled();
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ @Override
+ @Nullable
+ public Drawable getLogo() {
+ return mServiceInfo == null ? null : mServiceInfo.getWalletLogo(mContext);
+ }
+
+ @Override
+ @Nullable
+ public CharSequence getServiceLabel() {
+ return mServiceInfo == null ? null : mServiceInfo.getServiceLabel(mContext);
+ }
+
+ @Override
+ @Nullable
+ public CharSequence getShortcutShortLabel() {
+ return mServiceInfo == null ? null : mServiceInfo.getShortcutShortLabel(mContext);
+ }
+
+ @Override
+ public CharSequence getShortcutLongLabel() {
+ return mServiceInfo == null ? null : mServiceInfo.getShortcutLongLabel(mContext);
+ }
+
+ private void connect() {
+ mHandler.post(this::connectInternal);
+ }
+
+ private void connectInternal() {
+ if (mServiceInfo == null) {
+ Log.w(TAG, "Wallet service unavailable");
+ return;
+ }
+ if (mIsConnected) {
+ return;
+ }
+ mIsConnected = true;
+ Intent intent = new Intent(SERVICE_INTERFACE);
+ intent.setComponent(mServiceInfo.getComponentName());
+ int flags = Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY;
+ mContext.bindService(intent, this, flags);
+ resetServiceConnectionTimeout();
+ }
+
+ private void onConnectedInternal(IQuickAccessWalletService service) {
+ if (!mIsConnected) {
+ Log.w(TAG, "onConnectInternal but connection closed");
+ mService = null;
+ return;
+ }
+ mService = service;
+ for (ApiCaller apiCaller : new ArrayList<>(mRequestQueue)) {
+ performApiCallInternal(apiCaller, mService);
+ mRequestQueue.remove(apiCaller);
+ }
+ }
+
+ /**
+ * Resets the idle timeout for this connection by removing any pending timeout messages and
+ * posting a new delayed message.
+ */
+ private void resetServiceConnectionTimeout() {
+ mHandler.removeMessages(MSG_TIMEOUT_SERVICE);
+ mHandler.postDelayed(
+ () -> disconnectInternal(true),
+ MSG_TIMEOUT_SERVICE,
+ SERVICE_CONNECTION_TIMEOUT_MS);
+ }
+
+ private void disconnectInternal(boolean clearEventListeners) {
+ if (!mIsConnected) {
+ Log.w(TAG, "already disconnected");
+ return;
+ }
+ if (clearEventListeners && !mEventListeners.isEmpty()) {
+ for (WalletServiceEventListener listener : mEventListeners.keySet()) {
+ removeWalletServiceEventListener(listener);
+ }
+ mHandler.post(() -> disconnectInternal(false));
+ return;
+ }
+ mIsConnected = false;
+ mContext.unbindService(/*conn=*/this);
+ mService = null;
+ mEventListeners.clear();
+ mRequestQueue.clear();
+ }
+
+ private void executeApiCall(ApiCaller apiCaller) {
+ mHandler.post(() -> executeInternal(apiCaller));
+ }
+
+ private void executeInternal(ApiCaller apiCaller) {
+ if (mIsConnected && mService != null) {
+ performApiCallInternal(apiCaller, mService);
+ } else {
+ mRequestQueue.add(apiCaller);
+ connect();
+ }
+ }
+
+ private void performApiCallInternal(ApiCaller apiCaller, IQuickAccessWalletService service) {
+ if (service == null) {
+ apiCaller.onApiError();
+ return;
+ }
+ try {
+ apiCaller.performApiCall(service);
+ resetServiceConnectionTimeout();
+ } catch (RemoteException e) {
+ Log.w(TAG, "executeInternal error: " + apiCaller.mDesc, e);
+ apiCaller.onApiError();
+ disconnect();
+ }
+ }
+
+ private abstract static class ApiCaller {
+ private final String mDesc;
+
+ private ApiCaller(String desc) {
+ this.mDesc = desc;
+ }
+
+ abstract void performApiCall(IQuickAccessWalletService service)
+ throws RemoteException;
+
+ void onApiError() {
+ Log.w(TAG, "api error: " + mDesc);
+ }
+ }
+
+ @Override // ServiceConnection
+ public void onServiceConnected(ComponentName name, IBinder binder) {
+ IQuickAccessWalletService service = IQuickAccessWalletService.Stub.asInterface(binder);
+ mHandler.post(() -> onConnectedInternal(service));
+ }
+
+ @Override // ServiceConnection
+ public void onServiceDisconnected(ComponentName name) {
+ // Do not disconnect, as we may later be re-connected
+ }
+
+ @Override // ServiceConnection
+ public void onBindingDied(ComponentName name) {
+ // This is a recoverable error but the client will need to reconnect.
+ disconnect();
+ }
+
+ @Override // ServiceConnection
+ public void onNullBinding(ComponentName name) {
+ disconnect();
+ }
+
+ private boolean checkSecureSetting(String name) {
+ return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) == 1;
+ }
+
+ private boolean checkUserSetupComplete() {
+ return Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.USER_SETUP_COMPLETE, 0,
+ UserHandle.USER_CURRENT) == 1;
+ }
+
+ private static class BaseCallbacks extends IQuickAccessWalletServiceCallbacks.Stub {
+ public void onGetWalletCardsSuccess(GetWalletCardsResponse response) {
+ throw new IllegalStateException();
+ }
+
+ public void onGetWalletCardsFailure(GetWalletCardsError error) {
+ throw new IllegalStateException();
+ }
+
+ public void onWalletServiceEvent(WalletServiceEvent event) {
+ throw new IllegalStateException();
+ }
+ }
+}
diff --git a/android/service/quickaccesswallet/QuickAccessWalletService.java b/android/service/quickaccesswallet/QuickAccessWalletService.java
new file mode 100644
index 0000000..ef6150d
--- /dev/null
+++ b/android/service/quickaccesswallet/QuickAccessWalletService.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2020 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.quickaccesswallet;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+
+/**
+ * A {@code QuickAccessWalletService} provides a list of {@code WalletCard}s shown in the Quick
+ * Access Wallet. The Quick Access Wallet allows the user to change their selected payment method
+ * and access other important passes, such as tickets and transit passes, without leaving the
+ * context of their current app.
+ *
+ * <p>An {@code QuickAccessWalletService} is only bound to the Android System for the purposes of
+ * showing wallet cards if:
+ * <ol>
+ * <li>The application hosting the QuickAccessWalletService is also the default NFC payment
+ * application. This means that the same application must also have a
+ * {@link android.nfc.cardemulation.HostApduService} or
+ * {@link android.nfc.cardemulation.OffHostApduService} that requires the
+ * android.permission.BIND_NFC_SERVICE permission.
+ * <li>The user explicitly selected the application as the default payment application in
+ * the Tap & pay settings screen.
+ * <li>The QuickAccessWalletService requires that the binding application hold the
+ * {@code android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE} permission, which only the System
+ * Service can hold.
+ * <li>The user explicitly enables it using Android Settings (the
+ * {@link Settings#ACTION_QUICK_ACCESS_WALLET_SETTINGS} intent can be used to launch it).
+ * </ol>
+ *
+ * <a name="BasicUsage"></a>
+ * <h3>Basic usage</h3>
+ *
+ * <p>The basic Quick Access Wallet process is defined by the workflow below:
+ * <ol>
+ * <li>User performs a gesture to bring up the Quick Access Wallet, which is displayed by the
+ * Android System.
+ * <li>The Android System creates a {@link GetWalletCardsRequest}, binds to the
+ * {@link QuickAccessWalletService}, and delivers the request.
+ * <li>The service receives the request through {@link #onWalletCardsRequested}
+ * <li>The service responds by calling {@link GetWalletCardsCallback#onSuccess} with a
+ * {@link GetWalletCardsResponse response} that contains between 1 and
+ * {@link GetWalletCardsRequest#getMaxCards() maxCards} cards.
+ * <li>The Android System displays the Quick Access Wallet containing the provided cards. The
+ * card at the {@link GetWalletCardsResponse#getSelectedIndex() selectedIndex} will initially
+ * be presented as the 'selected' card.
+ * <li>As soon as the cards are displayed, the Android System will notify the service that the
+ * card at the selected index has been selected through {@link #onWalletCardSelected}.
+ * <li>The user interacts with the wallet and may select one or more cards in sequence. Each time
+ * a new card is selected, the Android System will notify the service through
+ * {@link #onWalletCardSelected} and will provide the {@link WalletCard#getCardId() cardId} of the
+ * card that is now selected.
+ * <li>If the user commences an NFC payment, the service may send a {@link WalletServiceEvent}
+ * to the System indicating that the wallet application now needs to show the activity associated
+ * with making a payment. Sending a {@link WalletServiceEvent} of type
+ * {@link WalletServiceEvent#TYPE_NFC_PAYMENT_STARTED} should cause the quick access wallet UI
+ * to be dismissed.
+ * <li>When the wallet is dismissed, the Android System will notify the service through
+ * {@link #onWalletDismissed}.
+ * </ol>
+ *
+ * <p>The workflow is designed to minimize the time that the Android System is bound to the
+ * service, but connections may be cached and reused to improve performance and conserve memory.
+ * All calls should be considered stateless: if the service needs to keep state between calls, it
+ * must do its own state management (keeping in mind that the service's process might be killed
+ * by the Android System when unbound; for example, if the device is running low in memory).
+ *
+ * <p>
+ * <a name="ErrorHandling"></a>
+ * <h3>Error handling</h3>
+ * <p>If the service encountered an error processing the request, it should call
+ * {@link GetWalletCardsCallback#onFailure}.
+ * For performance reasons, it's paramount that the service calls either
+ * {@link GetWalletCardsCallback#onSuccess} or
+ * {@link GetWalletCardsCallback#onFailure} for each
+ * {@link #onWalletCardsRequested} received - if it doesn't, the request will eventually time out
+ * and be discarded by the Android System.
+ *
+ * <p>
+ * <a name="ManifestEntry"></a>
+ * <h3>Manifest entry</h3>
+ *
+ * <p>QuickAccessWalletService must require the permission
+ * "android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE".
+ *
+ * <pre class="prettyprint">
+ * {@literal
+ * <service
+ * android:name=".MyQuickAccessWalletService"
+ * android:label="@string/my_default_tile_label"
+ * android:icon="@drawable/my_default_icon_label"
+ * android:logo="@drawable/my_wallet_logo"
+ * android:permission="android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.quickaccesswallet.QuickAccessWalletService" />
+ * <category android:name="android.intent.category.DEFAULT"/>
+ * </intent-filter>
+ * <meta-data android:name="android.quickaccesswallet"
+ * android:resource="@xml/quickaccesswallet_configuration" />;
+ * </service>}
+ * </pre>
+ * <p>
+ * The {@literal <meta-data>} element includes an android:resource attribute that points to an
+ * XML resource with further details about the service. The {@code quickaccesswallet_configuration}
+ * in the example above specifies an activity that allows the users to view the entire wallet.
+ * The following example shows the quickaccesswallet_configuration XML resource:
+ * <p>
+ * <pre class="prettyprint">
+ * {@literal
+ * <quickaccesswallet-service
+ * xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:settingsActivity="com.example.android.SettingsActivity"
+ * android:shortcutLongLabel="@string/my_wallet_empty_state_text"
+ * android:shortcutShortLabel="@string/my_wallet_button_text"
+ * android:targetActivity="com.example.android.WalletActivity"/>
+ * }
+ * </pre>
+ *
+ * <p>The entry for {@code settingsActivity} should contain the fully qualified class name of an
+ * activity that allows the user to modify the settings for this service. The {@code targetActivity}
+ * entry should contain the fully qualified class name of an activity that allows the user to view
+ * their entire wallet. The {@code targetActivity} will be started with the Intent action
+ * {@link #ACTION_VIEW_WALLET} and the {@code settingsActivity} will be started with the Intent
+ * action {@link #ACTION_VIEW_WALLET_SETTINGS}.
+ *
+ * <p>The {@code shortcutShortLabel} and {@code shortcutLongLabel} are used by the QuickAccessWallet
+ * in the buttons that navigate to the wallet app. The {@code shortcutShortLabel} is displayed next
+ * to the cards that are returned by the service and should be no more than 20 characters. The
+ * {@code shortcutLongLabel} is displayed when no cards are returned. This 'empty state' view also
+ * displays the service logo, specified by the {@code android:logo} manifest entry. If the logo is
+ * not specified, the empty state view will show the app icon instead.
+ */
+public abstract class QuickAccessWalletService extends Service {
+
+ private static final String TAG = "QAWalletService";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service. To be supported, the
+ * service must also require the
+ * {@link android.Manifest.permission#BIND_QUICK_ACCESS_WALLET_SERVICE}
+ * permission so that other applications can not abuse it.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE =
+ "android.service.quickaccesswallet.QuickAccessWalletService";
+
+ /**
+ * Intent action to launch an activity to display the wallet.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_VIEW_WALLET =
+ "android.service.quickaccesswallet.action.VIEW_WALLET";
+
+ /**
+ * Intent action to launch an activity to display quick access wallet settings.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_VIEW_WALLET_SETTINGS =
+ "android.service.quickaccesswallet.action.VIEW_WALLET_SETTINGS";
+
+ /**
+ * Name under which a QuickAccessWalletService component publishes information about itself.
+ * This meta-data should reference an XML resource containing a
+ * <code><{@link
+ * android.R.styleable#QuickAccessWalletService quickaccesswallet-service}></code> tag. This
+ * is a a sample XML file configuring an QuickAccessWalletService:
+ * <pre> <quickaccesswallet-service
+ * android:walletActivity="foo.bar.WalletActivity"
+ * . . .
+ * /></pre>
+ */
+ public static final String SERVICE_META_DATA = "android.quickaccesswallet";
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+ /**
+ * The service currently only supports one listener at a time. Multiple connections that
+ * register different listeners will clobber the listener. This field may only be accessed from
+ * the main thread.
+ */
+ @Nullable
+ private String mEventListenerId;
+
+ /**
+ * The service currently only supports one listener at a time. Multiple connections that
+ * register different listeners will clobber the listener. This field may only be accessed from
+ * the main thread.
+ */
+ @Nullable
+ private IQuickAccessWalletServiceCallbacks mEventListener;
+
+ private final IQuickAccessWalletService mInterface = new IQuickAccessWalletService.Stub() {
+ @Override
+ public void onWalletCardsRequested(
+ @NonNull GetWalletCardsRequest request,
+ @NonNull IQuickAccessWalletServiceCallbacks callback) {
+ mHandler.post(() -> onWalletCardsRequestedInternal(request, callback));
+ }
+
+ @Override
+ public void onWalletCardSelected(@NonNull SelectWalletCardRequest request) {
+ mHandler.post(() -> QuickAccessWalletService.this.onWalletCardSelected(request));
+ }
+
+ @Override
+ public void onWalletDismissed() {
+ mHandler.post(QuickAccessWalletService.this::onWalletDismissed);
+ }
+
+ public void registerWalletServiceEventListener(
+ @NonNull WalletServiceEventListenerRequest request,
+ @NonNull IQuickAccessWalletServiceCallbacks callback) {
+ mHandler.post(() -> registerDismissWalletListenerInternal(request, callback));
+ }
+
+ public void unregisterWalletServiceEventListener(
+ @NonNull WalletServiceEventListenerRequest request) {
+ mHandler.post(() -> unregisterDismissWalletListenerInternal(request));
+ }
+ };
+
+ private void onWalletCardsRequestedInternal(
+ GetWalletCardsRequest request,
+ IQuickAccessWalletServiceCallbacks callback) {
+ onWalletCardsRequested(request,
+ new GetWalletCardsCallbackImpl(request, callback, mHandler));
+ }
+
+ @Override
+ @Nullable
+ public IBinder onBind(@NonNull Intent intent) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ // Binding to the QuickAccessWalletService is protected by the
+ // android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE permission, which is defined in
+ // R. Pre-R devices can have other side-loaded applications that claim this permission.
+ // Ensures that the service is only enabled when properly permission protected.
+ Log.w(TAG, "Warning: binding on pre-R device");
+ }
+ if (!SERVICE_INTERFACE.equals(intent.getAction())) {
+ Log.w(TAG, "Wrong action");
+ return null;
+ }
+ return mInterface.asBinder();
+ }
+
+ /**
+ * Called when the user requests the service to provide wallet cards.
+ *
+ * <p>This method will be called on the main thread, but the callback may be called from any
+ * thread. The callback should be called as quickly as possible. The service must always call
+ * either {@link GetWalletCardsCallback#onSuccess(GetWalletCardsResponse)} or {@link
+ * GetWalletCardsCallback#onFailure(GetWalletCardsError)}. Calling multiple times or calling
+ * both methods will cause an exception to be thrown.
+ */
+ public abstract void onWalletCardsRequested(
+ @NonNull GetWalletCardsRequest request,
+ @NonNull GetWalletCardsCallback callback);
+
+ /**
+ * A wallet card was selected. Sent when the user selects a wallet card from the list of cards.
+ * Selection may indicate that the card is now in the center of the screen, or highlighted in
+ * some other fashion. It does not mean that the user clicked on the card -- clicking on the
+ * card will cause the {@link WalletCard#getPendingIntent()} to be sent.
+ *
+ * <p>Card selection events are especially important to NFC payment applications because
+ * many NFC terminals can only accept one payment card at a time. If the user has several NFC
+ * cards in their wallet, selecting different cards can change which payment method is presented
+ * to the terminal.
+ */
+ public abstract void onWalletCardSelected(@NonNull SelectWalletCardRequest request);
+
+ /**
+ * Indicates that the wallet was dismissed. This is received when the Quick Access Wallet is no
+ * longer visible.
+ */
+ public abstract void onWalletDismissed();
+
+ /**
+ * Send a {@link WalletServiceEvent} to the Quick Access Wallet.
+ * <p>
+ * Background events may require that the Quick Access Wallet view be updated. For example, if
+ * the wallet application hosting this service starts to handle an NFC payment while the Quick
+ * Access Wallet is being shown, the Quick Access Wallet will need to be dismissed so that the
+ * Activity showing the payment can be displayed to the user.
+ */
+ public final void sendWalletServiceEvent(@NonNull WalletServiceEvent serviceEvent) {
+ mHandler.post(() -> sendWalletServiceEventInternal(serviceEvent));
+ }
+
+ private void sendWalletServiceEventInternal(WalletServiceEvent serviceEvent) {
+ if (mEventListener == null) {
+ Log.i(TAG, "No dismiss listener registered");
+ return;
+ }
+ try {
+ mEventListener.onWalletServiceEvent(serviceEvent);
+ } catch (RemoteException e) {
+ Log.w(TAG, "onWalletServiceEvent error", e);
+ mEventListenerId = null;
+ mEventListener = null;
+ }
+ }
+
+ private void registerDismissWalletListenerInternal(
+ @NonNull WalletServiceEventListenerRequest request,
+ @NonNull IQuickAccessWalletServiceCallbacks callback) {
+ mEventListenerId = request.getListenerId();
+ mEventListener = callback;
+ }
+
+ private void unregisterDismissWalletListenerInternal(
+ @NonNull WalletServiceEventListenerRequest request) {
+ if (mEventListenerId != null && mEventListenerId.equals(request.getListenerId())) {
+ mEventListenerId = null;
+ mEventListener = null;
+ } else {
+ Log.w(TAG, "dismiss listener missing or replaced");
+ }
+ }
+}
diff --git a/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java b/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
new file mode 100644
index 0000000..f584bcd
--- /dev/null
+++ b/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2020 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.quickaccesswallet;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.internal.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * {@link ServiceInfo} and meta-data about a {@link QuickAccessWalletService}.
+ *
+ * @hide
+ */
+class QuickAccessWalletServiceInfo {
+
+ private static final String TAG = "QAWalletSInfo";
+ private static final String TAG_WALLET_SERVICE = "quickaccesswallet-service";
+
+ private final ServiceInfo mServiceInfo;
+ private final ServiceMetadata mServiceMetadata;
+
+ private QuickAccessWalletServiceInfo(
+ @NonNull ServiceInfo serviceInfo,
+ @NonNull ServiceMetadata metadata) {
+ mServiceInfo = serviceInfo;
+ mServiceMetadata = metadata;
+ }
+
+ @Nullable
+ static QuickAccessWalletServiceInfo tryCreate(@NonNull Context context) {
+ ComponentName defaultPaymentApp = getDefaultPaymentApp(context);
+ if (defaultPaymentApp == null) {
+ return null;
+ }
+
+ ServiceInfo serviceInfo = getWalletServiceInfo(context, defaultPaymentApp.getPackageName());
+ if (serviceInfo == null) {
+ return null;
+ }
+
+ if (!Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE.equals(serviceInfo.permission)) {
+ Log.w(TAG, String.format("%s.%s does not require permission %s",
+ serviceInfo.packageName, serviceInfo.name,
+ Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE));
+ return null;
+ }
+
+ ServiceMetadata metadata = parseServiceMetadata(context, serviceInfo);
+ return new QuickAccessWalletServiceInfo(serviceInfo, metadata);
+ }
+
+ private static ComponentName getDefaultPaymentApp(Context context) {
+ ContentResolver cr = context.getContentResolver();
+ String comp = Settings.Secure.getString(cr, Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT);
+ return comp == null ? null : ComponentName.unflattenFromString(comp);
+ }
+
+ private static ServiceInfo getWalletServiceInfo(Context context, String packageName) {
+ Intent intent = new Intent(QuickAccessWalletService.SERVICE_INTERFACE);
+ intent.setPackage(packageName);
+ List<ResolveInfo> resolveInfos =
+ context.getPackageManager().queryIntentServices(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ return resolveInfos.isEmpty() ? null : resolveInfos.get(0).serviceInfo;
+ }
+
+ private static class ServiceMetadata {
+ @Nullable
+ private final String mSettingsActivity;
+ @Nullable
+ private final String mTargetActivity;
+ @Nullable
+ private final CharSequence mShortcutShortLabel;
+ @Nullable
+ private final CharSequence mShortcutLongLabel;
+
+ private static ServiceMetadata empty() {
+ return new ServiceMetadata(null, null, null, null);
+ }
+
+ private ServiceMetadata(
+ String targetActivity,
+ String settingsActivity,
+ CharSequence shortcutShortLabel,
+ CharSequence shortcutLongLabel) {
+ mTargetActivity = targetActivity;
+ mSettingsActivity = settingsActivity;
+ mShortcutShortLabel = shortcutShortLabel;
+ mShortcutLongLabel = shortcutLongLabel;
+ }
+ }
+
+ private static ServiceMetadata parseServiceMetadata(Context context, ServiceInfo serviceInfo) {
+ PackageManager pm = context.getPackageManager();
+ final XmlResourceParser parser =
+ serviceInfo.loadXmlMetaData(pm, QuickAccessWalletService.SERVICE_META_DATA);
+
+ if (parser == null) {
+ return ServiceMetadata.empty();
+ }
+
+ try {
+ Resources resources = pm.getResourcesForApplication(serviceInfo.applicationInfo);
+ int type = 0;
+ while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
+ type = parser.next();
+ }
+
+ if (TAG_WALLET_SERVICE.equals(parser.getName())) {
+ final AttributeSet allAttributes = Xml.asAttributeSet(parser);
+ TypedArray afsAttributes = null;
+ try {
+ afsAttributes = resources.obtainAttributes(allAttributes,
+ R.styleable.QuickAccessWalletService);
+ String targetActivity = afsAttributes.getString(
+ R.styleable.QuickAccessWalletService_targetActivity);
+ String settingsActivity = afsAttributes.getString(
+ R.styleable.QuickAccessWalletService_settingsActivity);
+ CharSequence shortcutShortLabel = afsAttributes.getText(
+ R.styleable.QuickAccessWalletService_shortcutShortLabel);
+ CharSequence shortcutLongLabel = afsAttributes.getText(
+ R.styleable.QuickAccessWalletService_shortcutLongLabel);
+ return new ServiceMetadata(targetActivity, settingsActivity, shortcutShortLabel,
+ shortcutLongLabel);
+ } finally {
+ if (afsAttributes != null) {
+ afsAttributes.recycle();
+ }
+ }
+ } else {
+ Log.e(TAG, "Meta-data does not start with quickaccesswallet-service tag");
+ }
+ } catch (PackageManager.NameNotFoundException
+ | IOException
+ | XmlPullParserException e) {
+ Log.e(TAG, "Error parsing quickaccesswallet service meta-data", e);
+ }
+ return ServiceMetadata.empty();
+ }
+
+ /**
+ * @return the component name of the {@link QuickAccessWalletService}
+ */
+ @NonNull
+ ComponentName getComponentName() {
+ return mServiceInfo.getComponentName();
+ }
+
+ /**
+ * @return the fully qualified name of the activity that hosts the full wallet. If available,
+ * this intent should be started with the action
+ * {@link QuickAccessWalletService#ACTION_VIEW_WALLET}
+ */
+ @Nullable
+ String getWalletActivity() {
+ return mServiceMetadata.mTargetActivity;
+ }
+
+ /**
+ * @return the fully qualified name of the activity that allows the user to change quick access
+ * wallet settings. If available, this intent should be started with the action {@link
+ * QuickAccessWalletService#ACTION_VIEW_WALLET_SETTINGS}
+ */
+ @Nullable
+ String getSettingsActivity() {
+ return mServiceMetadata.mSettingsActivity;
+ }
+
+ @NonNull
+ Drawable getWalletLogo(Context context) {
+ Drawable drawable = mServiceInfo.loadLogo(context.getPackageManager());
+ if (drawable != null) {
+ return drawable;
+ }
+ return mServiceInfo.loadIcon(context.getPackageManager());
+ }
+
+ @NonNull
+ CharSequence getShortcutShortLabel(Context context) {
+ if (!TextUtils.isEmpty(mServiceMetadata.mShortcutShortLabel)) {
+ return mServiceMetadata.mShortcutShortLabel;
+ }
+ return mServiceInfo.loadLabel(context.getPackageManager());
+ }
+
+ @NonNull
+ CharSequence getShortcutLongLabel(Context context) {
+ if (!TextUtils.isEmpty(mServiceMetadata.mShortcutLongLabel)) {
+ return mServiceMetadata.mShortcutLongLabel;
+ }
+ return mServiceInfo.loadLabel(context.getPackageManager());
+ }
+
+ @NonNull
+ CharSequence getServiceLabel(Context context) {
+ return mServiceInfo.loadLabel(context.getPackageManager());
+ }
+}
diff --git a/android/service/quickaccesswallet/SelectWalletCardRequest.java b/android/service/quickaccesswallet/SelectWalletCardRequest.java
new file mode 100644
index 0000000..cb69eee
--- /dev/null
+++ b/android/service/quickaccesswallet/SelectWalletCardRequest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 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.quickaccesswallet;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents a request to a {@link QuickAccessWalletService} to select a particular {@link
+ * WalletCard walletCard}. Card selection events are transmitted to the WalletService so that the
+ * selected card may be used by the NFC payment service.
+ */
+public final class SelectWalletCardRequest implements Parcelable {
+
+ private final String mCardId;
+
+ /**
+ * Creates a new GetWalletCardsRequest.
+ *
+ * @param cardId The {@link WalletCard#getCardId() cardId} of the wallet card that is currently
+ * selected.
+ */
+ public SelectWalletCardRequest(@NonNull String cardId) {
+ this.mCardId = cardId;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mCardId);
+ }
+
+ @NonNull
+ public static final Creator<SelectWalletCardRequest> CREATOR =
+ new Creator<SelectWalletCardRequest>() {
+ @Override
+ public SelectWalletCardRequest createFromParcel(Parcel source) {
+ String cardId = source.readString();
+ return new SelectWalletCardRequest(cardId);
+ }
+
+ @Override
+ public SelectWalletCardRequest[] newArray(int size) {
+ return new SelectWalletCardRequest[size];
+ }
+ };
+
+ /**
+ * The {@link WalletCard#getCardId() cardId} of the wallet card that is currently selected.
+ */
+ @NonNull
+ public String getCardId() {
+ return mCardId;
+ }
+}
diff --git a/android/service/quickaccesswallet/WalletCard.java b/android/service/quickaccesswallet/WalletCard.java
new file mode 100644
index 0000000..b09d2e9
--- /dev/null
+++ b/android/service/quickaccesswallet/WalletCard.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2020 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.quickaccesswallet;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * A {@link WalletCard} can represent anything that a user might carry in their wallet -- a credit
+ * card, library card, transit pass, etc. Cards are identified by a String identifier and contain a
+ * card image, card image content description, and a {@link PendingIntent} to be used if the user
+ * clicks on the card. Cards may be displayed with an icon and label, though these are optional.
+ */
+public final class WalletCard implements Parcelable {
+
+ private final String mCardId;
+ private final Icon mCardImage;
+ private final CharSequence mContentDescription;
+ private final PendingIntent mPendingIntent;
+ private final Icon mCardIcon;
+ private final CharSequence mCardLabel;
+
+ private WalletCard(Builder builder) {
+ this.mCardId = builder.mCardId;
+ this.mCardImage = builder.mCardImage;
+ this.mContentDescription = builder.mContentDescription;
+ this.mPendingIntent = builder.mPendingIntent;
+ this.mCardIcon = builder.mCardIcon;
+ this.mCardLabel = builder.mCardLabel;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mCardId);
+ mCardImage.writeToParcel(dest, flags);
+ TextUtils.writeToParcel(mContentDescription, dest, flags);
+ PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest);
+ if (mCardIcon == null) {
+ dest.writeByte((byte) 0);
+ } else {
+ dest.writeByte((byte) 1);
+ mCardIcon.writeToParcel(dest, flags);
+ }
+ TextUtils.writeToParcel(mCardLabel, dest, flags);
+ }
+
+ private static WalletCard readFromParcel(Parcel source) {
+ String cardId = source.readString();
+ Icon cardImage = Icon.CREATOR.createFromParcel(source);
+ CharSequence contentDesc = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ PendingIntent pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(source);
+ Icon cardIcon = source.readByte() == 0 ? null : Icon.CREATOR.createFromParcel(source);
+ CharSequence cardLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ return new Builder(cardId, cardImage, contentDesc, pendingIntent)
+ .setCardIcon(cardIcon)
+ .setCardLabel(cardLabel)
+ .build();
+ }
+
+ @NonNull
+ public static final Creator<WalletCard> CREATOR =
+ new Creator<WalletCard>() {
+ @Override
+ public WalletCard createFromParcel(Parcel source) {
+ return readFromParcel(source);
+ }
+
+ @Override
+ public WalletCard[] newArray(int size) {
+ return new WalletCard[size];
+ }
+ };
+
+ /**
+ * The card id must be unique within the list of cards returned.
+ */
+ @NonNull
+ public String getCardId() {
+ return mCardId;
+ }
+
+ /**
+ * The visual representation of the card. If the card image Icon is a bitmap, it should have a
+ * width of {@link GetWalletCardsRequest#getCardWidthPx()} and a height of {@link
+ * GetWalletCardsRequest#getCardHeightPx()}.
+ */
+ @NonNull
+ public Icon getCardImage() {
+ return mCardImage;
+ }
+
+ /**
+ * The content description of the card image.
+ */
+ @NonNull
+ public CharSequence getContentDescription() {
+ return mContentDescription;
+ }
+
+ /**
+ * If the user performs a click on the card, this PendingIntent will be sent. If the device is
+ * locked, the wallet will first request device unlock before sending the pending intent.
+ */
+ @NonNull
+ public PendingIntent getPendingIntent() {
+ return mPendingIntent;
+ }
+
+ /**
+ * An icon may be shown alongside the card image to convey information about how the card can be
+ * used, or if some other action must be taken before using the card. For example, an NFC logo
+ * could indicate that the card is NFC-enabled and will be provided to an NFC terminal if the
+ * phone is held in close proximity to the NFC reader.
+ *
+ * <p>If the supplied Icon is backed by a bitmap, it should have width and height
+ * {@link GetWalletCardsRequest#getIconSizePx()}.
+ */
+ @Nullable
+ public Icon getCardIcon() {
+ return mCardIcon;
+ }
+
+ /**
+ * A card label may be shown alongside the card image to convey information about how the card
+ * can be used, or if some other action must be taken before using the card. For example, an
+ * NFC-enabled card could be labeled "Hold near reader" to inform the user of how to use NFC
+ * cards when interacting with an NFC reader.
+ *
+ * <p>If the provided label is too long to fit on one line, it may be truncated and ellipsized.
+ */
+ @Nullable
+ public CharSequence getCardLabel() {
+ return mCardLabel;
+ }
+
+ /**
+ * Builder for {@link WalletCard} objects. You must to provide cardId, cardImage,
+ * contentDescription, and pendingIntent. If the card is opaque and should be shown with
+ * elevation, set hasShadow to true. cardIcon and cardLabel are optional.
+ */
+ public static final class Builder {
+ private String mCardId;
+ private Icon mCardImage;
+ private CharSequence mContentDescription;
+ private PendingIntent mPendingIntent;
+ private Icon mCardIcon;
+ private CharSequence mCardLabel;
+
+ /**
+ * @param cardId The card id must be non-null and unique within the list of
+ * cards returned. <b>Note:
+ * </b> this card ID should <b>not</b> contain PII (Personally
+ * Identifiable Information, such as username or email address).
+ * @param cardImage The visual representation of the card. If the card image Icon
+ * is a bitmap, it should have a width of {@link
+ * GetWalletCardsRequest#getCardWidthPx()} and a height of {@link
+ * GetWalletCardsRequest#getCardHeightPx()}. If the card image
+ * does not have these dimensions, it may appear distorted when it
+ * is scaled to fit these dimensions on screen. Bitmaps must be
+ * of type {@link android.graphics.Bitmap.Config#HARDWARE} for
+ * performance reasons.
+ * @param contentDescription The content description of the card image. This field is
+ * required and may not be null or empty.
+ * <b>Note: </b> this message should <b>not</b> contain PII
+ * (Personally Identifiable Information, such as username or email
+ * address).
+ * @param pendingIntent If the user performs a click on the card, this PendingIntent
+ * will be sent. If the device is locked, the wallet will first
+ * request device unlock before sending the pending intent. It is
+ * recommended that the pending intent be immutable (use {@link
+ * PendingIntent#FLAG_IMMUTABLE}).
+ */
+ public Builder(@NonNull String cardId,
+ @NonNull Icon cardImage,
+ @NonNull CharSequence contentDescription,
+ @NonNull PendingIntent pendingIntent) {
+ mCardId = cardId;
+ mCardImage = cardImage;
+ mContentDescription = contentDescription;
+ mPendingIntent = pendingIntent;
+ }
+
+ /**
+ * An icon may be shown alongside the card image to convey information about how the card
+ * can be used, or if some other action must be taken before using the card. For example, an
+ * NFC logo could indicate that the card is NFC-enabled and will be provided to an NFC
+ * terminal if the phone is held in close proximity to the NFC reader. This field is
+ * optional.
+ *
+ * <p>If the supplied Icon is backed by a bitmap, it should have width and height
+ * {@link GetWalletCardsRequest#getIconSizePx()}.
+ */
+ @NonNull
+ public Builder setCardIcon(@Nullable Icon cardIcon) {
+ mCardIcon = cardIcon;
+ return this;
+ }
+
+ /**
+ * A card label may be shown alongside the card image to convey information about how the
+ * card can be used, or if some other action must be taken before using the card. For
+ * example, an NFC-enabled card could be labeled "Hold near reader" to inform the user of
+ * how to use NFC cards when interacting with an NFC reader. This field is optional.
+ * <b>Note: </b> this card label should <b>not</b> contain PII (Personally Identifiable
+ * Information, such as username or email address). If the provided label is too long to fit
+ * on one line, it may be truncated and ellipsized.
+ */
+ @NonNull
+ public Builder setCardLabel(@Nullable CharSequence cardLabel) {
+ mCardLabel = cardLabel;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link WalletCard} instance.
+ *
+ * @return A built response.
+ */
+ @NonNull
+ public WalletCard build() {
+ return new WalletCard(this);
+ }
+ }
+}
diff --git a/android/service/quickaccesswallet/WalletServiceEvent.java b/android/service/quickaccesswallet/WalletServiceEvent.java
new file mode 100644
index 0000000..5ee92da
--- /dev/null
+++ b/android/service/quickaccesswallet/WalletServiceEvent.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2020 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.quickaccesswallet;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents a request from the {@link QuickAccessWalletService wallet app} to the Quick Access
+ * Wallet in System UI. Background events may necessitate that the Quick Access Wallet update its
+ * view. For example, if the wallet application handles an NFC payment while the Quick Access Wallet
+ * is being shown, it needs to tell the Quick Access Wallet so that the wallet can be dismissed and
+ * Activity showing the payment can be displayed to the user.
+ */
+public final class WalletServiceEvent implements Parcelable {
+
+ /**
+ * An NFC payment has started. If the Quick Access Wallet is in a system window, it will need to
+ * be dismissed so that an Activity showing the payment can be displayed.
+ */
+ public static final int TYPE_NFC_PAYMENT_STARTED = 1;
+
+ /**
+ * Indicates that the wallet cards have changed and should be refreshed.
+ * @hide
+ */
+ public static final int TYPE_WALLET_CARDS_UPDATED = 2;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_NFC_PAYMENT_STARTED, TYPE_WALLET_CARDS_UPDATED})
+ public @interface EventType {
+ }
+
+ @EventType
+ private final int mEventType;
+
+ /**
+ * Creates a new DismissWalletRequest.
+ */
+ public WalletServiceEvent(@EventType int eventType) {
+ this.mEventType = eventType;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mEventType);
+ }
+
+ @NonNull
+ public static final Creator<WalletServiceEvent> CREATOR =
+ new Creator<WalletServiceEvent>() {
+ @Override
+ public WalletServiceEvent createFromParcel(Parcel source) {
+ int eventType = source.readInt();
+ return new WalletServiceEvent(eventType);
+ }
+
+ @Override
+ public WalletServiceEvent[] newArray(int size) {
+ return new WalletServiceEvent[size];
+ }
+ };
+
+ /**
+ * @return the event type
+ */
+ @EventType
+ public int getEventType() {
+ return mEventType;
+ }
+}
diff --git a/android/service/quickaccesswallet/WalletServiceEventListenerRequest.java b/android/service/quickaccesswallet/WalletServiceEventListenerRequest.java
new file mode 100644
index 0000000..223110e
--- /dev/null
+++ b/android/service/quickaccesswallet/WalletServiceEventListenerRequest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 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.quickaccesswallet;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Register a dismiss request listener with the QuickAccessWalletService. This allows the service to
+ * dismiss the wallet if it needs to show a payment activity in response to an NFC event.
+ *
+ * @hide
+ */
+public final class WalletServiceEventListenerRequest implements Parcelable {
+
+ private final String mListenerId;
+
+ /**
+ * Construct a new {@code DismissWalletListenerRequest}.
+ *
+ * @param listenerKey A unique key that identifies the listener.
+ */
+ public WalletServiceEventListenerRequest(@NonNull String listenerKey) {
+ mListenerId = listenerKey;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mListenerId);
+ }
+
+ private static WalletServiceEventListenerRequest readFromParcel(Parcel source) {
+ String listenerId = source.readString();
+ return new WalletServiceEventListenerRequest(listenerId);
+ }
+
+ @NonNull
+ public static final Creator<WalletServiceEventListenerRequest> CREATOR =
+ new Creator<WalletServiceEventListenerRequest>() {
+ @Override
+ public WalletServiceEventListenerRequest createFromParcel(Parcel source) {
+ return readFromParcel(source);
+ }
+
+ @Override
+ public WalletServiceEventListenerRequest[] newArray(int size) {
+ return new WalletServiceEventListenerRequest[size];
+ }
+ };
+
+ /**
+ * Returns the unique key that identifies the wallet dismiss request listener.
+ */
+ @NonNull
+ public String getListenerId() {
+ return mListenerId;
+ }
+}
diff --git a/android/service/quicksettings/Tile.java b/android/service/quicksettings/Tile.java
new file mode 100644
index 0000000..40c0ac0
--- /dev/null
+++ b/android/service/quicksettings/Tile.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2015 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.quicksettings;
+
+import android.annotation.Nullable;
+import android.graphics.drawable.Icon;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * A Tile holds the state of a tile that will be displayed
+ * in Quick Settings.
+ *
+ * A tile in Quick Settings exists as an icon with an accompanied label.
+ * It also may have content description for accessibility usability.
+ * The style and layout of the tile may change to match a given
+ * device.
+ */
+public final class Tile implements Parcelable {
+
+ private static final String TAG = "Tile";
+
+ /**
+ * An unavailable state indicates that for some reason this tile is not currently
+ * available to the user for some reason, and will have no click action. The tile's
+ * icon will be tinted differently to reflect this state.
+ */
+ public static final int STATE_UNAVAILABLE = 0;
+
+ /**
+ * This represents a tile that is currently in a disabled state but is still interactable.
+ *
+ * A disabled state indicates that the tile is not currently active (e.g. wifi disconnected or
+ * bluetooth disabled), but is still interactable by the user to modify this state. Tiles
+ * that have boolean states should use this to represent one of their states. The tile's
+ * icon will be tinted differently to reflect this state, but still be distinct from unavailable.
+ */
+ public static final int STATE_INACTIVE = 1;
+
+ /**
+ * This represents a tile that is currently active. (e.g. wifi is connected, bluetooth is on,
+ * cast is casting). This is the default state.
+ */
+ public static final int STATE_ACTIVE = 2;
+
+ private IBinder mToken;
+ private Icon mIcon;
+ private CharSequence mLabel;
+ private CharSequence mSubtitle;
+ private CharSequence mContentDescription;
+ private CharSequence mStateDescription;
+ // Default to inactive until clients of the new API can update.
+ private int mState = STATE_INACTIVE;
+
+ private IQSService mService;
+
+ /**
+ * @hide
+ */
+ public Tile(Parcel source) {
+ readFromParcel(source);
+ }
+
+ /**
+ * @hide
+ */
+ public Tile() {
+ }
+
+ /**
+ * @hide
+ */
+ public void setService(IQSService service, IBinder stub) {
+ mService = service;
+ mToken = stub;
+ }
+
+ /**
+ * The current state of the tile.
+ *
+ * @see #STATE_UNAVAILABLE
+ * @see #STATE_INACTIVE
+ * @see #STATE_ACTIVE
+ */
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * Sets the current state for the tile.
+ *
+ * Does not take effect until {@link #updateTile()} is called.
+ *
+ * @param state One of {@link #STATE_UNAVAILABLE}, {@link #STATE_INACTIVE},
+ * {@link #STATE_ACTIVE}
+ */
+ public void setState(int state) {
+ mState = state;
+ }
+
+ /**
+ * Gets the current icon for the tile.
+ */
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * Sets the current icon for the tile.
+ *
+ * This icon is expected to be white on alpha, and may be
+ * tinted by the system to match it's theme.
+ *
+ * Does not take effect until {@link #updateTile()} is called.
+ *
+ * @param icon New icon to show.
+ */
+ public void setIcon(Icon icon) {
+ this.mIcon = icon;
+ }
+
+ /**
+ * Gets the current label for the tile.
+ */
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ /**
+ * Sets the current label for the tile.
+ *
+ * Does not take effect until {@link #updateTile()} is called.
+ *
+ * @param label New label to show.
+ */
+ public void setLabel(CharSequence label) {
+ this.mLabel = label;
+ }
+
+ /**
+ * Gets the current subtitle for the tile.
+ */
+ @Nullable
+ public CharSequence getSubtitle() {
+ return mSubtitle;
+ }
+
+ /**
+ * Set the subtitle for the tile. Will be displayed as the secondary label.
+ * @param subtitle the subtitle to show.
+ */
+ public void setSubtitle(@Nullable CharSequence subtitle) {
+ this.mSubtitle = subtitle;
+ }
+
+ /**
+ * Gets the current content description for the tile.
+ */
+ public CharSequence getContentDescription() {
+ return mContentDescription;
+ }
+
+ /**
+ * Gets the current state description for the tile.
+ */
+ @Nullable
+ public CharSequence getStateDescription() {
+ return mStateDescription;
+ }
+
+ /**
+ * Sets the current content description for the tile.
+ *
+ * Does not take effect until {@link #updateTile()} is called.
+ *
+ * @param contentDescription New content description to use.
+ */
+ public void setContentDescription(CharSequence contentDescription) {
+ this.mContentDescription = contentDescription;
+ }
+
+ /**
+ * Sets the current state description for the tile.
+ *
+ * Does not take effect until {@link #updateTile()} is called.
+ *
+ * @param stateDescription New state description to use.
+ */
+ public void setStateDescription(@Nullable CharSequence stateDescription) {
+ this.mStateDescription = stateDescription;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Pushes the state of the Tile to Quick Settings to be displayed.
+ */
+ public void updateTile() {
+ try {
+ mService.updateQsTile(this, mToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't update tile");
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mIcon != null) {
+ dest.writeByte((byte) 1);
+ mIcon.writeToParcel(dest, flags);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ dest.writeInt(mState);
+ TextUtils.writeToParcel(mLabel, dest, flags);
+ TextUtils.writeToParcel(mSubtitle, dest, flags);
+ TextUtils.writeToParcel(mContentDescription, dest, flags);
+ TextUtils.writeToParcel(mStateDescription, dest, flags);
+ }
+
+ private void readFromParcel(Parcel source) {
+ if (source.readByte() != 0) {
+ mIcon = Icon.CREATOR.createFromParcel(source);
+ } else {
+ mIcon = null;
+ }
+ mState = source.readInt();
+ mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ mSubtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ mStateDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ }
+
+ public static final @android.annotation.NonNull Creator<Tile> CREATOR = new Creator<Tile>() {
+ @Override
+ public Tile createFromParcel(Parcel source) {
+ return new Tile(source);
+ }
+
+ @Override
+ public Tile[] newArray(int size) {
+ return new Tile[size];
+ }
+ };
+}
\ No newline at end of file
diff --git a/android/service/quicksettings/TileService.java b/android/service/quicksettings/TileService.java
new file mode 100644
index 0000000..b4b5819
--- /dev/null
+++ b/android/service/quicksettings/TileService.java
@@ -0,0 +1,492 @@
+/*
+ * Copyright (C) 2015 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.quicksettings;
+
+import android.Manifest;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Dialog;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
+import android.view.WindowManager;
+
+import com.android.internal.R;
+
+/**
+ * A TileService provides the user a tile that can be added to Quick Settings.
+ * Quick Settings is a space provided that allows the user to change settings and
+ * take quick actions without leaving the context of their current app.
+ *
+ * <p>The lifecycle of a TileService is different from some other services in
+ * that it may be unbound during parts of its lifecycle. Any of the following
+ * lifecycle events can happen indepently in a separate binding/creation of the
+ * service.</p>
+ *
+ * <ul>
+ * <li>When a tile is added by the user its TileService will be bound to and
+ * {@link #onTileAdded()} will be called.</li>
+ *
+ * <li>When a tile should be up to date and listing will be indicated by
+ * {@link #onStartListening()} and {@link #onStopListening()}.</li>
+ *
+ * <li>When the user removes a tile from Quick Settings {@link #onTileRemoved()}
+ * will be called.</li>
+ * </ul>
+ * <p>TileService will be detected by tiles that match the {@value #ACTION_QS_TILE}
+ * and require the permission "android.permission.BIND_QUICK_SETTINGS_TILE".
+ * The label and icon for the service will be used as the default label and
+ * icon for the tile. Here is an example TileService declaration.</p>
+ * <pre class="prettyprint">
+ * {@literal
+ * <service
+ * android:name=".MyQSTileService"
+ * android:label="@string/my_default_tile_label"
+ * android:icon="@drawable/my_default_icon_label"
+ * android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+ * <intent-filter>
+ * <action android:name="android.service.quicksettings.action.QS_TILE" />
+ * </intent-filter>
+ * </service>}
+ * </pre>
+ *
+ * @see Tile Tile for details about the UI of a Quick Settings Tile.
+ */
+public class TileService extends Service {
+
+ private static final String TAG = "TileService";
+ private static final boolean DEBUG = false;
+
+ /**
+ * An activity that provides a user interface for adjusting TileService
+ * preferences. Optional but recommended for apps that implement a
+ * TileService.
+ * <p>
+ * This intent may also define a {@link Intent#EXTRA_COMPONENT_NAME} value
+ * to indicate the {@link ComponentName} that caused the preferences to be
+ * opened.
+ * <p>
+ * To ensure that the activity can only be launched through quick settings
+ * UI provided by this service, apps can protect it with the
+ * BIND_QUICK_SETTINGS_TILE permission.
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String ACTION_QS_TILE_PREFERENCES
+ = "android.service.quicksettings.action.QS_TILE_PREFERENCES";
+
+ /**
+ * Action that identifies a Service as being a TileService.
+ */
+ public static final String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE";
+
+ /**
+ * Meta-data for tile definition to set a tile into active mode.
+ * <p>
+ * Active mode is for tiles which already listen and keep track of their state in their
+ * own process. These tiles may request to send an update to the System while their process
+ * is alive using {@link #requestListeningState}. The System will only bind these tiles
+ * on its own when a click needs to occur.
+ *
+ * To make a TileService an active tile, set this meta-data to true on the TileService's
+ * manifest declaration.
+ * <pre class="prettyprint">
+ * {@literal
+ * <meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
+ * android:value="true" />
+ * }
+ * </pre>
+ */
+ public static final String META_DATA_ACTIVE_TILE
+ = "android.service.quicksettings.ACTIVE_TILE";
+
+ /**
+ * Meta-data for a tile to mark is toggleable.
+ * <p>
+ * Toggleable tiles support switch tile behavior in accessibility. This is
+ * the behavior of most of the framework tiles.
+ *
+ * To indicate that a TileService is toggleable, set this meta-data to true on the
+ * TileService's manifest declaration.
+ * <pre class="prettyprint">
+ * {@literal
+ * <meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
+ * android:value="true" />
+ * }
+ * </pre>
+ */
+ public static final String META_DATA_TOGGLEABLE_TILE =
+ "android.service.quicksettings.TOGGLEABLE_TILE";
+
+ /**
+ * Used to notify SysUI that Listening has be requested.
+ * @hide
+ */
+ public static final String ACTION_REQUEST_LISTENING =
+ "android.service.quicksettings.action.REQUEST_LISTENING";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_SERVICE = "service";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_TOKEN = "token";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_STATE = "state";
+
+ private final H mHandler = new H(Looper.getMainLooper());
+
+ private boolean mListening = false;
+ private Tile mTile;
+ private IBinder mToken;
+ private IQSService mService;
+ private Runnable mUnlockRunnable;
+ private IBinder mTileToken;
+
+ @Override
+ public void onDestroy() {
+ if (mListening) {
+ onStopListening();
+ mListening = false;
+ }
+ super.onDestroy();
+ }
+
+ /**
+ * Called when the user adds this tile to Quick Settings.
+ * <p/>
+ * Note that this is not guaranteed to be called between {@link #onCreate()}
+ * and {@link #onStartListening()}, it will only be called when the tile is added
+ * and not on subsequent binds.
+ */
+ public void onTileAdded() {
+ }
+
+ /**
+ * Called when the user removes this tile from Quick Settings.
+ */
+ public void onTileRemoved() {
+ }
+
+ /**
+ * Called when this tile moves into a listening state.
+ * <p/>
+ * When this tile is in a listening state it is expected to keep the
+ * UI up to date. Any listeners or callbacks needed to keep this tile
+ * up to date should be registered here and unregistered in {@link #onStopListening()}.
+ *
+ * @see #getQsTile()
+ * @see Tile#updateTile()
+ */
+ public void onStartListening() {
+ }
+
+ /**
+ * Called when this tile moves out of the listening state.
+ */
+ public void onStopListening() {
+ }
+
+ /**
+ * Called when the user clicks on this tile.
+ */
+ public void onClick() {
+ }
+
+ /**
+ * Sets an icon to be shown in the status bar.
+ * <p>
+ * The icon will be displayed before all other icons. Can only be called between
+ * {@link #onStartListening} and {@link #onStopListening}. Can only be called by system apps.
+ *
+ * @param icon The icon to be displayed, null to hide
+ * @param contentDescription Content description of the icon to be displayed
+ * @hide
+ */
+ @SystemApi
+ public final void setStatusIcon(Icon icon, String contentDescription) {
+ if (mService != null) {
+ try {
+ mService.updateStatusIcon(mTileToken, icon, contentDescription);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /**
+ * Used to show a dialog.
+ *
+ * This will collapse the Quick Settings panel and show the dialog.
+ *
+ * @param dialog Dialog to show.
+ *
+ * @see #isLocked()
+ */
+ public final void showDialog(Dialog dialog) {
+ dialog.getWindow().getAttributes().token = mToken;
+ dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_QS_DIALOG);
+ dialog.getWindow().getDecorView().addOnAttachStateChangeListener(
+ new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ try {
+ mService.onDialogHidden(mTileToken);
+ } catch (RemoteException e) {
+ }
+ }
+ });
+ dialog.show();
+ try {
+ mService.onShowDialog(mTileToken);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Prompts the user to unlock the device before executing the Runnable.
+ * <p>
+ * The user will be prompted for their current security method if applicable
+ * and if successful, runnable will be executed. The Runnable will not be
+ * executed if the user fails to unlock the device or cancels the operation.
+ */
+ public final void unlockAndRun(Runnable runnable) {
+ mUnlockRunnable = runnable;
+ try {
+ mService.startUnlockAndRun(mTileToken);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Checks if the device is in a secure state.
+ *
+ * TileServices should detect when the device is secure and change their behavior
+ * accordingly.
+ *
+ * @return true if the device is secure.
+ */
+ public final boolean isSecure() {
+ try {
+ return mService.isSecure();
+ } catch (RemoteException e) {
+ return true;
+ }
+ }
+
+ /**
+ * Checks if the lock screen is showing.
+ *
+ * When a device is locked, then {@link #showDialog} will not present a dialog, as it will
+ * be under the lock screen. If the behavior of the Tile is safe to do while locked,
+ * then the user should use {@link #startActivity} to launch an activity on top of the lock
+ * screen, otherwise the tile should use {@link #unlockAndRun(Runnable)} to give the
+ * user their security challenge.
+ *
+ * @return true if the device is locked.
+ */
+ public final boolean isLocked() {
+ try {
+ return mService.isLocked();
+ } catch (RemoteException e) {
+ return true;
+ }
+ }
+
+ /**
+ * Start an activity while collapsing the panel.
+ */
+ public final void startActivityAndCollapse(Intent intent) {
+ startActivity(intent);
+ try {
+ mService.onStartActivity(mTileToken);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Gets the {@link Tile} for this service.
+ * <p/>
+ * This tile may be used to get or set the current state for this
+ * tile. This tile is only valid for updates between {@link #onStartListening()}
+ * and {@link #onStopListening()}.
+ */
+ public final Tile getQsTile() {
+ return mTile;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ mService = IQSService.Stub.asInterface(intent.getIBinderExtra(EXTRA_SERVICE));
+ mTileToken = intent.getIBinderExtra(EXTRA_TOKEN);
+ try {
+ mTile = mService.getTile(mTileToken);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Unable to reach IQSService", e);
+ }
+ if (mTile != null) {
+ mTile.setService(mService, mTileToken);
+ mHandler.sendEmptyMessage(H.MSG_START_SUCCESS);
+ }
+ return new IQSTileService.Stub() {
+ @Override
+ public void onTileRemoved() throws RemoteException {
+ mHandler.sendEmptyMessage(H.MSG_TILE_REMOVED);
+ }
+
+ @Override
+ public void onTileAdded() throws RemoteException {
+ mHandler.sendEmptyMessage(H.MSG_TILE_ADDED);
+ }
+
+ @Override
+ public void onStopListening() throws RemoteException {
+ mHandler.sendEmptyMessage(H.MSG_STOP_LISTENING);
+ }
+
+ @Override
+ public void onStartListening() throws RemoteException {
+ mHandler.sendEmptyMessage(H.MSG_START_LISTENING);
+ }
+
+ @Override
+ public void onClick(IBinder wtoken) throws RemoteException {
+ mHandler.obtainMessage(H.MSG_TILE_CLICKED, wtoken).sendToTarget();
+ }
+
+ @Override
+ public void onUnlockComplete() throws RemoteException{
+ mHandler.sendEmptyMessage(H.MSG_UNLOCK_COMPLETE);
+ }
+ };
+ }
+
+ private class H extends Handler {
+ private static final int MSG_START_LISTENING = 1;
+ private static final int MSG_STOP_LISTENING = 2;
+ private static final int MSG_TILE_ADDED = 3;
+ private static final int MSG_TILE_REMOVED = 4;
+ private static final int MSG_TILE_CLICKED = 5;
+ private static final int MSG_UNLOCK_COMPLETE = 6;
+ private static final int MSG_START_SUCCESS = 7;
+ private final String mTileServiceName;
+
+ public H(Looper looper) {
+ super(looper);
+ mTileServiceName = TileService.this.getClass().getSimpleName();
+ }
+
+ private void logMessage(String message) {
+ Log.d(TAG, mTileServiceName + " Handler - " + message);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_TILE_ADDED:
+ if (DEBUG) logMessage("MSG_TILE_ADDED");
+ TileService.this.onTileAdded();
+ break;
+ case MSG_TILE_REMOVED:
+ if (DEBUG) logMessage("MSG_TILE_REMOVED");
+ if (mListening) {
+ mListening = false;
+ TileService.this.onStopListening();
+ }
+ TileService.this.onTileRemoved();
+ break;
+ case MSG_STOP_LISTENING:
+ if (DEBUG) logMessage("MSG_STOP_LISTENING");
+ if (mListening) {
+ mListening = false;
+ TileService.this.onStopListening();
+ }
+ break;
+ case MSG_START_LISTENING:
+ if (DEBUG) logMessage("MSG_START_LISTENING");
+ if (!mListening) {
+ mListening = true;
+ TileService.this.onStartListening();
+ }
+ break;
+ case MSG_TILE_CLICKED:
+ if (DEBUG) logMessage("MSG_TILE_CLICKED");
+ mToken = (IBinder) msg.obj;
+ TileService.this.onClick();
+ break;
+ case MSG_UNLOCK_COMPLETE:
+ if (DEBUG) logMessage("MSG_UNLOCK_COMPLETE");
+ if (mUnlockRunnable != null) {
+ mUnlockRunnable.run();
+ }
+ break;
+ case MSG_START_SUCCESS:
+ if (DEBUG) logMessage("MSG_START_SUCCESS");
+ try {
+ mService.onStartSuccessful(mTileToken);
+ } catch (RemoteException e) {
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * @return True if the device supports quick settings and its assocated APIs.
+ * @hide
+ */
+ @TestApi
+ public static boolean isQuickSettingsSupported() {
+ return Resources.getSystem().getBoolean(R.bool.config_quickSettingsSupported);
+ }
+
+ /**
+ * Requests that a tile be put in the listening state so it can send an update.
+ *
+ * This method is only applicable to tiles that have {@link #META_DATA_ACTIVE_TILE} defined
+ * as true on their TileService Manifest declaration, and will do nothing otherwise.
+ */
+ public static final void requestListeningState(Context context, ComponentName component) {
+ final ComponentName sysuiComponent = ComponentName.unflattenFromString(
+ context.getResources().getString(
+ com.android.internal.R.string.config_systemUIServiceComponent));
+ Intent intent = new Intent(ACTION_REQUEST_LISTENING);
+ intent.putExtra(Intent.EXTRA_COMPONENT_NAME, component);
+ intent.setPackage(sysuiComponent.getPackageName());
+ context.sendBroadcast(intent, Manifest.permission.BIND_QUICK_SETTINGS_TILE);
+ }
+}
diff --git a/android/service/resolver/ResolverRankerService.java b/android/service/resolver/ResolverRankerService.java
new file mode 100644
index 0000000..7523347
--- /dev/null
+++ b/android/service/resolver/ResolverRankerService.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2017 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.resolver;
+
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.RemoteException;
+import android.service.resolver.ResolverTarget;
+import android.util.Log;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A service to rank apps according to usage stats of apps, when the system is resolving targets for
+ * an Intent.
+ *
+ * <p>To extend this class, you must declare the service in your manifest file with the
+ * {@link android.Manifest.permission#BIND_RESOLVER_RANKER_SERVICE} permission, and include an
+ * intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
+ * <pre>
+ * <service android:name=".MyResolverRankerService"
+ * android:exported="true"
+ * android:priority="100"
+ * android:permission="android.permission.BIND_RESOLVER_RANKER_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.resolver.ResolverRankerService" />
+ * </intent-filter>
+ * </service>
+ * </pre>
+ * @hide
+ */
+@SystemApi
+public abstract class ResolverRankerService extends Service {
+
+ private static final String TAG = "ResolverRankerService";
+
+ private static final boolean DEBUG = false;
+
+ /**
+ * The Intent action that a service must respond to. Add it to the intent filter of the service
+ * in its manifest.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.service.resolver.ResolverRankerService";
+
+ /**
+ * The permission that a service must hold. If the service does not hold the permission, the
+ * system will skip that service.
+ */
+ public static final String HOLD_PERMISSION = "android.permission.PROVIDE_RESOLVER_RANKER_SERVICE";
+
+ /**
+ * The permission that a service must require to ensure that only Android system can bind to it.
+ * If this permission is not enforced in the AndroidManifest of the service, the system will
+ * skip that service.
+ */
+ public static final String BIND_PERMISSION = "android.permission.BIND_RESOLVER_RANKER_SERVICE";
+
+ private ResolverRankerServiceWrapper mWrapper = null;
+
+ /**
+ * Called by the system to retrieve a list of probabilities to rank apps/options. To implement
+ * it, set selectProbability of each input {@link ResolverTarget}. The higher the
+ * selectProbability is, the more likely the {@link ResolverTarget} will be selected by the
+ * user. Override this function to provide prediction results.
+ *
+ * @param targets a list of {@link ResolverTarget}, for the list of apps to be ranked.
+ *
+ * @throws Exception when the prediction task fails.
+ */
+ public void onPredictSharingProbabilities(final List<ResolverTarget> targets) {}
+
+ /**
+ * Called by the system to train/update a ranking service, after the user makes a selection from
+ * the ranked list of apps. Override this function to enable model updates.
+ *
+ * @param targets a list of {@link ResolverTarget}, for the list of apps to be ranked.
+ * @param selectedPosition the position of the selected app in the list.
+ *
+ * @throws Exception when the training task fails.
+ */
+ public void onTrainRankingModel(
+ final List<ResolverTarget> targets, final int selectedPosition) {}
+
+ private static final String HANDLER_THREAD_NAME = "RESOLVER_RANKER_SERVICE";
+ private volatile Handler mHandler;
+ private HandlerThread mHandlerThread;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (DEBUG) Log.d(TAG, "onBind " + intent);
+ if (!SERVICE_INTERFACE.equals(intent.getAction())) {
+ if (DEBUG) Log.d(TAG, "bad intent action " + intent.getAction() + "; returning null");
+ return null;
+ }
+ if (mHandlerThread == null) {
+ mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ }
+ if (mWrapper == null) {
+ mWrapper = new ResolverRankerServiceWrapper();
+ }
+ return mWrapper;
+ }
+
+ @Override
+ public void onDestroy() {
+ mHandler = null;
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ }
+ super.onDestroy();
+ }
+
+ private static void sendResult(List<ResolverTarget> targets, IResolverRankerResult result) {
+ try {
+ result.sendResult(targets);
+ } catch (Exception e) {
+ Log.e(TAG, "failed to send results: " + e);
+ }
+ }
+
+ private class ResolverRankerServiceWrapper extends IResolverRankerService.Stub {
+
+ @Override
+ public void predict(final List<ResolverTarget> targets, final IResolverRankerResult result)
+ throws RemoteException {
+ Runnable predictRunnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "predict calls onPredictSharingProbabilities.");
+ }
+ onPredictSharingProbabilities(targets);
+ sendResult(targets, result);
+ } catch (Exception e) {
+ Log.e(TAG, "onPredictSharingProbabilities failed; send null results: " + e);
+ sendResult(null, result);
+ }
+ }
+ };
+ final Handler h = mHandler;
+ if (h != null) {
+ h.post(predictRunnable);
+ }
+ }
+
+ @Override
+ public void train(final List<ResolverTarget> targets, final int selectedPosition)
+ throws RemoteException {
+ Runnable trainRunnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "train calls onTranRankingModel");
+ }
+ onTrainRankingModel(targets, selectedPosition);
+ } catch (Exception e) {
+ Log.e(TAG, "onTrainRankingModel failed; skip train: " + e);
+ }
+ }
+ };
+ final Handler h = mHandler;
+ if (h != null) {
+ h.post(trainRunnable);
+ }
+ }
+ }
+}
diff --git a/android/service/resolver/ResolverTarget.java b/android/service/resolver/ResolverTarget.java
new file mode 100644
index 0000000..b3657c4
--- /dev/null
+++ b/android/service/resolver/ResolverTarget.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2017 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.resolver;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A ResolverTarget contains features by which an app or option will be ranked, in
+ * {@link ResolverRankerService}.
+ * @hide
+ */
+@SystemApi
+public final class ResolverTarget implements Parcelable {
+ private static final String TAG = "ResolverTarget";
+
+ /**
+ * a float score for recency of last use.
+ */
+ private float mRecencyScore;
+
+ /**
+ * a float score for total time spent.
+ */
+ private float mTimeSpentScore;
+
+ /**
+ * a float score for number of launches.
+ */
+ private float mLaunchScore;
+
+ /**
+ * a float score for number of selected.
+ */
+ private float mChooserScore;
+
+ /**
+ * a float score for the probability to be selected.
+ */
+ private float mSelectProbability;
+
+ // constructor for the class.
+ public ResolverTarget() {}
+
+ ResolverTarget(Parcel in) {
+ mRecencyScore = in.readFloat();
+ mTimeSpentScore = in.readFloat();
+ mLaunchScore = in.readFloat();
+ mChooserScore = in.readFloat();
+ mSelectProbability = in.readFloat();
+ }
+
+ /**
+ * Gets the score for how recently the target was used in the foreground.
+ *
+ * @return a float score whose range is [0, 1]. The higher the score is, the more recently the
+ * target was used.
+ */
+ public float getRecencyScore() {
+ return mRecencyScore;
+ }
+
+ /**
+ * Sets the score for how recently the target was used in the foreground.
+ *
+ * @param recencyScore a float score whose range is [0, 1]. The higher the score is, the more
+ * recently the target was used.
+ */
+ public void setRecencyScore(float recencyScore) {
+ this.mRecencyScore = recencyScore;
+ }
+
+ /**
+ * Gets the score for how long the target has been used in the foreground.
+ *
+ * @return a float score whose range is [0, 1]. The higher the score is, the longer the target
+ * has been used for.
+ */
+ public float getTimeSpentScore() {
+ return mTimeSpentScore;
+ }
+
+ /**
+ * Sets the score for how long the target has been used in the foreground.
+ *
+ * @param timeSpentScore a float score whose range is [0, 1]. The higher the score is, the
+ * longer the target has been used for.
+ */
+ public void setTimeSpentScore(float timeSpentScore) {
+ this.mTimeSpentScore = timeSpentScore;
+ }
+
+ /**
+ * Gets the score for how many times the target has been launched to the foreground.
+ *
+ * @return a float score whose range is [0, 1]. The higher the score is, the more times the
+ * target has been launched.
+ */
+ public float getLaunchScore() {
+ return mLaunchScore;
+ }
+
+ /**
+ * Sets the score for how many times the target has been launched to the foreground.
+ *
+ * @param launchScore a float score whose range is [0, 1]. The higher the score is, the more
+ * times the target has been launched.
+ */
+ public void setLaunchScore(float launchScore) {
+ this.mLaunchScore = launchScore;
+ }
+
+ /**
+ * Gets the score for how many times the target has been selected by the user to share the same
+ * types of content.
+ *
+ * @return a float score whose range is [0, 1]. The higher the score is, the
+ * more times the target has been selected by the user to share the same types of content for.
+ */
+ public float getChooserScore() {
+ return mChooserScore;
+ }
+
+ /**
+ * Sets the score for how many times the target has been selected by the user to share the same
+ * types of content.
+ *
+ * @param chooserScore a float score whose range is [0, 1]. The higher the score is, the more
+ * times the target has been selected by the user to share the same types
+ * of content for.
+ */
+ public void setChooserScore(float chooserScore) {
+ this.mChooserScore = chooserScore;
+ }
+
+ /**
+ * Gets the probability of how likely this target will be selected by the user.
+ *
+ * @return a float score whose range is [0, 1]. The higher the score is, the more likely the
+ * user is going to select this target.
+ */
+ public float getSelectProbability() {
+ return mSelectProbability;
+ }
+
+ /**
+ * Sets the probability for how like this target will be selected by the user.
+ *
+ * @param selectProbability a float score whose range is [0, 1]. The higher the score is, the
+ * more likely tht user is going to select this target.
+ */
+ public void setSelectProbability(float selectProbability) {
+ this.mSelectProbability = selectProbability;
+ }
+
+ // serialize the class to a string.
+ @NonNull
+ @Override
+ public String toString() {
+ return "ResolverTarget{"
+ + mRecencyScore + ", "
+ + mTimeSpentScore + ", "
+ + mLaunchScore + ", "
+ + mChooserScore + ", "
+ + mSelectProbability + "}";
+ }
+
+ // describes the kinds of special objects contained in this Parcelable instance's marshaled
+ // representation.
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ // flattens this object in to a Parcel.
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeFloat(mRecencyScore);
+ dest.writeFloat(mTimeSpentScore);
+ dest.writeFloat(mLaunchScore);
+ dest.writeFloat(mChooserScore);
+ dest.writeFloat(mSelectProbability);
+ }
+
+ // creator definition for the class.
+ public static final @android.annotation.NonNull Creator<ResolverTarget> CREATOR
+ = new Creator<ResolverTarget>() {
+ @Override
+ public ResolverTarget createFromParcel(Parcel source) {
+ return new ResolverTarget(source);
+ }
+
+ @Override
+ public ResolverTarget[] newArray(int size) {
+ return new ResolverTarget[size];
+ }
+ };
+}
diff --git a/android/service/restrictions/RestrictionsReceiver.java b/android/service/restrictions/RestrictionsReceiver.java
new file mode 100644
index 0000000..e8d481a
--- /dev/null
+++ b/android/service/restrictions/RestrictionsReceiver.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2014 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.restrictions;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.RestrictionsManager;
+import android.os.PersistableBundle;
+
+/**
+ * Abstract implementation of a Restrictions Provider BroadcastReceiver. To implement a
+ * Restrictions Provider, extend from this class and implement the abstract methods.
+ * Export this receiver in the manifest. A profile owner device admin can then register this
+ * component as a Restrictions Provider using
+ * {@link DevicePolicyManager#setRestrictionsProvider(ComponentName, ComponentName)}.
+ * <p>
+ * The function of a Restrictions Provider is to transport permission requests from apps on this
+ * device to an administrator (most likely on a remote device or computer) and deliver back
+ * responses. The response should be sent back to the app via
+ * {@link RestrictionsManager#notifyPermissionResponse(String, PersistableBundle)}.
+ *
+ * @see RestrictionsManager
+ */
+public abstract class RestrictionsReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "RestrictionsReceiver";
+
+ /**
+ * An asynchronous permission request made by an application for an operation that requires
+ * authorization by a local or remote administrator other than the user. The Restrictions
+ * Provider should transfer the request to the administrator and deliver back a response, when
+ * available. The calling application is aware that the response could take an indefinite
+ * amount of time.
+ * <p>
+ * If the request bundle contains the key {@link RestrictionsManager#REQUEST_KEY_NEW_REQUEST},
+ * then a new request must be sent. Otherwise the provider can look up any previous response
+ * to the same requestId and return the cached response.
+ *
+ * @param packageName the application requesting permission.
+ * @param requestType the type of request, which determines the content and presentation of
+ * the request data.
+ * @param request the request data bundle containing at a minimum a request id.
+ *
+ * @see RestrictionsManager#REQUEST_TYPE_APPROVAL
+ * @see RestrictionsManager#REQUEST_KEY_ID
+ */
+ public abstract void onRequestPermission(Context context,
+ String packageName, String requestType, String requestId, PersistableBundle request);
+
+ /**
+ * Intercept standard Restrictions Provider broadcasts. Implementations
+ * should not override this method; it is better to implement the
+ * convenience callbacks for each action.
+ */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ if (RestrictionsManager.ACTION_REQUEST_PERMISSION.equals(action)) {
+ String packageName = intent.getStringExtra(RestrictionsManager.EXTRA_PACKAGE_NAME);
+ String requestType = intent.getStringExtra(RestrictionsManager.EXTRA_REQUEST_TYPE);
+ String requestId = intent.getStringExtra(RestrictionsManager.EXTRA_REQUEST_ID);
+ PersistableBundle request = (PersistableBundle)
+ intent.getParcelableExtra(RestrictionsManager.EXTRA_REQUEST_BUNDLE);
+ onRequestPermission(context, packageName, requestType, requestId, request);
+ }
+ }
+}
diff --git a/android/service/settings/suggestions/Suggestion.java b/android/service/settings/suggestions/Suggestion.java
new file mode 100644
index 0000000..3e63efb
--- /dev/null
+++ b/android/service/settings/suggestions/Suggestion.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2017 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.suggestions;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Data object that has information about a device suggestion.
+ *
+ * @hide
+ */
+@SystemApi
+public final class Suggestion implements Parcelable {
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = true, prefix = { "FLAG_" }, value = {
+ FLAG_HAS_BUTTON,
+ FLAG_ICON_TINTABLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Flags {
+ }
+
+ /**
+ * Flag for suggestion type with a single button
+ */
+ public static final int FLAG_HAS_BUTTON = 1 << 0;
+ /**
+ * @hide
+ */
+ public static final int FLAG_ICON_TINTABLE = 1 << 1;
+
+ private final String mId;
+ private final CharSequence mTitle;
+ private final CharSequence mSummary;
+ private final Icon mIcon;
+ @Flags
+ private final int mFlags;
+ private final PendingIntent mPendingIntent;
+
+ /**
+ * Gets the id for the suggestion object.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Title of the suggestion that is shown to the user.
+ */
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Optional summary describing what this suggestion controls.
+ */
+ public CharSequence getSummary() {
+ return mSummary;
+ }
+
+ /**
+ * Optional icon for this suggestion.
+ */
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * Optional flags for this suggestion. This will influence UI when rendering suggestion in
+ * different style.
+ */
+ @Flags
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * The Intent to launch when the suggestion is activated.
+ */
+ public PendingIntent getPendingIntent() {
+ return mPendingIntent;
+ }
+
+ private Suggestion(Builder builder) {
+ mId = builder.mId;
+ mTitle = builder.mTitle;
+ mSummary = builder.mSummary;
+ mIcon = builder.mIcon;
+ mFlags = builder.mFlags;
+ mPendingIntent = builder.mPendingIntent;
+ }
+
+ private Suggestion(Parcel in) {
+ mId = in.readString();
+ mTitle = in.readCharSequence();
+ mSummary = in.readCharSequence();
+ mIcon = in.readParcelable(Icon.class.getClassLoader());
+ mFlags = in.readInt();
+ mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader());
+ }
+
+ public static final @android.annotation.NonNull Creator<Suggestion> CREATOR = new Creator<Suggestion>() {
+ @Override
+ public Suggestion createFromParcel(Parcel in) {
+ return new Suggestion(in);
+ }
+
+ @Override
+ public Suggestion[] newArray(int size) {
+ return new Suggestion[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mId);
+ dest.writeCharSequence(mTitle);
+ dest.writeCharSequence(mSummary);
+ dest.writeParcelable(mIcon, flags);
+ dest.writeInt(mFlags);
+ dest.writeParcelable(mPendingIntent, flags);
+ }
+
+ /**
+ * Builder class for {@link Suggestion}.
+ */
+ public static class Builder {
+ private final String mId;
+ private CharSequence mTitle;
+ private CharSequence mSummary;
+ private Icon mIcon;
+ @Flags
+ private int mFlags;
+ private PendingIntent mPendingIntent;
+
+ public Builder(String id) {
+ if (TextUtils.isEmpty(id)) {
+ throw new IllegalArgumentException("Suggestion id cannot be empty");
+ }
+ mId = id;
+ }
+
+ /**
+ * Sets suggestion title
+ */
+ public Builder setTitle(CharSequence title) {
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * Sets suggestion summary
+ */
+ public Builder setSummary(CharSequence summary) {
+ mSummary = summary;
+ return this;
+ }
+
+ /**
+ * Sets icon for the suggestion.
+ */
+ public Builder setIcon(Icon icon) {
+ mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Sets a UI type for this suggestion. This will influence UI when rendering suggestion in
+ * different style.
+ */
+ public Builder setFlags(@Flags int flags) {
+ mFlags = flags;
+ return this;
+ }
+
+ /**
+ * Sets suggestion intent
+ */
+ public Builder setPendingIntent(PendingIntent pendingIntent) {
+ mPendingIntent = pendingIntent;
+ return this;
+ }
+
+ /**
+ * Builds an immutable {@link Suggestion} object.
+ */
+ public Suggestion build() {
+ return new Suggestion(this /* builder */);
+ }
+ }
+}
diff --git a/android/service/settings/suggestions/SuggestionService.java b/android/service/settings/suggestions/SuggestionService.java
new file mode 100644
index 0000000..ce9501d
--- /dev/null
+++ b/android/service/settings/suggestions/SuggestionService.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 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.suggestions;
+
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * This is the base class for implementing suggestion service. A suggestion service is responsible
+ * to provide a collection of {@link Suggestion}s for the current user when queried.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class SuggestionService extends Service {
+
+ private static final String TAG = "SuggestionService";
+ private static final boolean DEBUG = false;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new ISuggestionService.Stub() {
+ @Override
+ public List<Suggestion> getSuggestions() {
+ if (DEBUG) {
+ Log.d(TAG, "getSuggestions() " + getPackageName());
+ }
+ return onGetSuggestions();
+ }
+
+ @Override
+ public void dismissSuggestion(Suggestion suggestion) {
+ if (DEBUG) {
+ Log.d(TAG, "dismissSuggestion() " + getPackageName());
+ }
+ onSuggestionDismissed(suggestion);
+ }
+
+ @Override
+ public void launchSuggestion(Suggestion suggestion) {
+ if (DEBUG) {
+ Log.d(TAG, "launchSuggestion() " + getPackageName());
+ }
+ onSuggestionLaunched(suggestion);
+ }
+ };
+ }
+
+ /**
+ * Return all available suggestions.
+ */
+ public abstract List<Suggestion> onGetSuggestions();
+
+ /**
+ * Dismiss a suggestion. The suggestion will not be included in future
+ * {@link #onGetSuggestions()} calls.
+ */
+ public abstract void onSuggestionDismissed(Suggestion suggestion);
+
+ /**
+ * This is the opposite signal to {@link #onSuggestionDismissed}, indicating a suggestion has
+ * been launched.
+ */
+ public abstract void onSuggestionLaunched(Suggestion suggestion);
+}
diff --git a/android/service/storage/ExternalStorageService.java b/android/service/storage/ExternalStorageService.java
new file mode 100644
index 0000000..3b4d84a
--- /dev/null
+++ b/android/service/storage/ExternalStorageService.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2019 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.storage;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelableException;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.storage.StorageVolume;
+
+import com.android.internal.os.BackgroundThread;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A service to handle filesystem I/O from other apps.
+ *
+ * <p>To extend this class, you must declare the service in your manifest file with the
+ * {@link android.Manifest.permission#BIND_EXTERNAL_STORAGE_SERVICE} permission,
+ * and include an intent filter with the {@link #SERVICE_INTERFACE} action.
+ * For example:</p>
+ * <pre>
+ * <service android:name=".ExternalStorageServiceImpl"
+ * android:exported="true"
+ * android:priority="100"
+ * android:permission="android.permission.BIND_EXTERNAL_STORAGE_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.storage.ExternalStorageService" />
+ * </intent-filter>
+ * </service>
+ * </pre>
+ * @hide
+ */
+@SystemApi
+public abstract class ExternalStorageService extends Service {
+ /**
+ * The Intent action that a service must respond to. Add it as an intent filter in the
+ * manifest declaration of the implementing service.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.service.storage.ExternalStorageService";
+ /**
+ * Whether the session associated with the device file descriptor when calling
+ * {@link #onStartSession} is a FUSE session.
+ */
+ public static final int FLAG_SESSION_TYPE_FUSE = 1 << 0;
+
+ /**
+ * Whether the upper file system path specified when calling {@link #onStartSession}
+ * should be indexed.
+ */
+ public static final int FLAG_SESSION_ATTRIBUTE_INDEXABLE = 1 << 1;
+
+ /**
+ * {@link Bundle} key for a {@link String} value.
+ *
+ * {@hide}
+ */
+ public static final String EXTRA_SESSION_ID =
+ "android.service.storage.extra.session_id";
+ /**
+ * {@link Bundle} key for a {@link ParcelableException} value.
+ *
+ * {@hide}
+ */
+ public static final String EXTRA_ERROR =
+ "android.service.storage.extra.error";
+
+ /** @hide */
+ @IntDef(flag = true, prefix = {"FLAG_SESSION_"},
+ value = {FLAG_SESSION_TYPE_FUSE, FLAG_SESSION_ATTRIBUTE_INDEXABLE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SessionFlag {}
+
+ private final ExternalStorageServiceWrapper mWrapper = new ExternalStorageServiceWrapper();
+ private final Handler mHandler = BackgroundThread.getHandler();
+
+ /**
+ * Called when the system starts a session associated with {@code deviceFd}
+ * identified by {@code sessionId} to handle filesystem I/O for other apps. The type of
+ * session and other attributes are passed in {@code flag}.
+ *
+ * <p> I/O is received as requests originating from {@code upperFileSystemPath} on
+ * {@code deviceFd}. Implementors should handle the I/O by responding to these requests
+ * using the data on the {@code lowerFileSystemPath}.
+ *
+ * <p> Additional calls to start a session for the same {@code sessionId} while the session
+ * is still starting or already started should have no effect.
+ *
+ * @param sessionId uniquely identifies a running session and used in {@link #onEndSession}
+ * @param flag specifies the type or additional attributes of a session
+ * @param deviceFd for intercepting IO from other apps
+ * @param upperFileSystemPath is the root path on which we are intercepting IO from other apps
+ * @param lowerFileSystemPath is the root path matching {@code upperFileSystemPath} containing
+ * the actual data apps are trying to access
+ */
+ public abstract void onStartSession(@NonNull String sessionId, @SessionFlag int flag,
+ @NonNull ParcelFileDescriptor deviceFd, @NonNull File upperFileSystemPath,
+ @NonNull File lowerFileSystemPath) throws IOException;
+
+ /**
+ * Called when the system ends the session identified by {@code sessionId}. Implementors should
+ * stop handling filesystem I/O and clean up resources from the ended session.
+ *
+ * <p> Additional calls to end a session for the same {@code sessionId} while the session
+ * is still ending or has not started should have no effect.
+ */
+ public abstract void onEndSession(@NonNull String sessionId) throws IOException;
+
+ /**
+ * Called when any volume's state changes.
+ *
+ * <p> This is required to communicate volume state changes with the Storage Service before
+ * broadcasting to other apps. The Storage Service needs to process any change in the volume
+ * state (before other apps receive a broadcast for the same) to update the database so that
+ * other apps have the correct view of the volume.
+ *
+ * <p> Blocks until the Storage Service processes/scans the volume or fails in doing so.
+ *
+ * @param vol name of the volume that was changed
+ */
+ public abstract void onVolumeStateChanged(@NonNull StorageVolume vol) throws IOException;
+
+ @Override
+ @NonNull
+ public final IBinder onBind(@NonNull Intent intent) {
+ return mWrapper;
+ }
+
+ private class ExternalStorageServiceWrapper extends IExternalStorageService.Stub {
+ @Override
+ public void startSession(String sessionId, @SessionFlag int flag,
+ ParcelFileDescriptor deviceFd, String upperPath, String lowerPath,
+ RemoteCallback callback) throws RemoteException {
+ mHandler.post(() -> {
+ try {
+ onStartSession(sessionId, flag, deviceFd, new File(upperPath),
+ new File(lowerPath));
+ sendResult(sessionId, null /* throwable */, callback);
+ } catch (Throwable t) {
+ sendResult(sessionId, t, callback);
+ }
+ });
+ }
+
+ @Override
+ public void notifyVolumeStateChanged(String sessionId, StorageVolume vol,
+ RemoteCallback callback) {
+ mHandler.post(() -> {
+ try {
+ onVolumeStateChanged(vol);
+ sendResult(sessionId, null /* throwable */, callback);
+ } catch (Throwable t) {
+ sendResult(sessionId, t, callback);
+ }
+ });
+ }
+
+ @Override
+ public void endSession(String sessionId, RemoteCallback callback) throws RemoteException {
+ mHandler.post(() -> {
+ try {
+ onEndSession(sessionId);
+ sendResult(sessionId, null /* throwable */, callback);
+ } catch (Throwable t) {
+ sendResult(sessionId, t, callback);
+ }
+ });
+ }
+
+ private void sendResult(String sessionId, Throwable throwable, RemoteCallback callback) {
+ Bundle bundle = new Bundle();
+ bundle.putString(EXTRA_SESSION_ID, sessionId);
+ if (throwable != null) {
+ bundle.putParcelable(EXTRA_ERROR, new ParcelableException(throwable));
+ }
+ callback.sendResult(bundle);
+ }
+ }
+}
diff --git a/android/service/textclassifier/TextClassifierService.java b/android/service/textclassifier/TextClassifierService.java
new file mode 100644
index 0000000..93faa58
--- /dev/null
+++ b/android/service/textclassifier/TextClassifierService.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2018 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.textclassifier;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.view.textclassifier.ConversationActions;
+import android.view.textclassifier.SelectionEvent;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassificationContext;
+import android.view.textclassifier.TextClassificationManager;
+import android.view.textclassifier.TextClassificationSessionId;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextClassifierEvent;
+import android.view.textclassifier.TextLanguage;
+import android.view.textclassifier.TextLinks;
+import android.view.textclassifier.TextSelection;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Abstract base class for the TextClassifier service.
+ *
+ * <p>A TextClassifier service provides text classification related features for the system.
+ * The system's default TextClassifierService provider is configured in
+ * {@code config_defaultTextClassifierPackage}. If this config has no value, a
+ * {@link android.view.textclassifier.TextClassifierImpl} is loaded in the calling app's process.
+ *
+ * <p>See: {@link TextClassifier}.
+ * See: {@link TextClassificationManager}.
+ *
+ * <p>Include the following in the manifest:
+ *
+ * <pre>
+ * {@literal
+ * <service android:name=".YourTextClassifierService"
+ * android:permission="android.permission.BIND_TEXTCLASSIFIER_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.textclassifier.TextClassifierService" />
+ * </intent-filter>
+ * </service>}</pre>
+ *
+ * <p>From {@link android.os.Build.VERSION_CODES#Q} onward, all callbacks are called on the main
+ * thread. Prior to Q, there is no guarantee on what thread the callback will happen. You should
+ * make sure the callbacks are executed in your desired thread by using a executor, a handler or
+ * something else along the line.
+ *
+ * @see TextClassifier
+ * @hide
+ */
+@SystemApi
+@TestApi
+public abstract class TextClassifierService extends Service {
+
+ private static final String LOG_TAG = "TextClassifierService";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_TEXTCLASSIFIER_SERVICE} permission so
+ * that other applications can not abuse it.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.textclassifier.TextClassifierService";
+
+ /** @hide **/
+ public static final int CONNECTED = 0;
+ /** @hide **/
+ public static final int DISCONNECTED = 1;
+ /** @hide */
+ @IntDef(value = {
+ CONNECTED,
+ DISCONNECTED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ConnectionState{}
+
+ /** @hide **/
+ private static final String KEY_RESULT = "key_result";
+
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper(), null, true);
+ private final ExecutorService mSingleThreadExecutor = Executors.newSingleThreadExecutor();
+
+ private final ITextClassifierService.Stub mBinder = new ITextClassifierService.Stub() {
+
+ // TODO(b/72533911): Implement cancellation signal
+ @NonNull private final CancellationSignal mCancellationSignal = new CancellationSignal();
+
+ @Override
+ public void onSuggestSelection(
+ TextClassificationSessionId sessionId,
+ TextSelection.Request request, ITextClassifierCallback callback) {
+ Preconditions.checkNotNull(request);
+ Preconditions.checkNotNull(callback);
+ mMainThreadHandler.post(() -> TextClassifierService.this.onSuggestSelection(
+ sessionId, request, mCancellationSignal, new ProxyCallback<>(callback)));
+
+ }
+
+ @Override
+ public void onClassifyText(
+ TextClassificationSessionId sessionId,
+ TextClassification.Request request, ITextClassifierCallback callback) {
+ Preconditions.checkNotNull(request);
+ Preconditions.checkNotNull(callback);
+ mMainThreadHandler.post(() -> TextClassifierService.this.onClassifyText(
+ sessionId, request, mCancellationSignal, new ProxyCallback<>(callback)));
+ }
+
+ @Override
+ public void onGenerateLinks(
+ TextClassificationSessionId sessionId,
+ TextLinks.Request request, ITextClassifierCallback callback) {
+ Preconditions.checkNotNull(request);
+ Preconditions.checkNotNull(callback);
+ mMainThreadHandler.post(() -> TextClassifierService.this.onGenerateLinks(
+ sessionId, request, mCancellationSignal, new ProxyCallback<>(callback)));
+ }
+
+ @Override
+ public void onSelectionEvent(
+ TextClassificationSessionId sessionId,
+ SelectionEvent event) {
+ Preconditions.checkNotNull(event);
+ mMainThreadHandler.post(
+ () -> TextClassifierService.this.onSelectionEvent(sessionId, event));
+ }
+
+ @Override
+ public void onTextClassifierEvent(
+ TextClassificationSessionId sessionId,
+ TextClassifierEvent event) {
+ Preconditions.checkNotNull(event);
+ mMainThreadHandler.post(
+ () -> TextClassifierService.this.onTextClassifierEvent(sessionId, event));
+ }
+
+ @Override
+ public void onDetectLanguage(
+ TextClassificationSessionId sessionId,
+ TextLanguage.Request request,
+ ITextClassifierCallback callback) {
+ Preconditions.checkNotNull(request);
+ Preconditions.checkNotNull(callback);
+ mMainThreadHandler.post(() -> TextClassifierService.this.onDetectLanguage(
+ sessionId, request, mCancellationSignal, new ProxyCallback<>(callback)));
+ }
+
+ @Override
+ public void onSuggestConversationActions(
+ TextClassificationSessionId sessionId,
+ ConversationActions.Request request,
+ ITextClassifierCallback callback) {
+ Preconditions.checkNotNull(request);
+ Preconditions.checkNotNull(callback);
+ mMainThreadHandler.post(() -> TextClassifierService.this.onSuggestConversationActions(
+ sessionId, request, mCancellationSignal, new ProxyCallback<>(callback)));
+ }
+
+ @Override
+ public void onCreateTextClassificationSession(
+ TextClassificationContext context, TextClassificationSessionId sessionId) {
+ Preconditions.checkNotNull(context);
+ Preconditions.checkNotNull(sessionId);
+ mMainThreadHandler.post(
+ () -> TextClassifierService.this.onCreateTextClassificationSession(
+ context, sessionId));
+ }
+
+ @Override
+ public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId) {
+ mMainThreadHandler.post(
+ () -> TextClassifierService.this.onDestroyTextClassificationSession(sessionId));
+ }
+
+ @Override
+ public void onConnectedStateChanged(@ConnectionState int connected) {
+ mMainThreadHandler.post(connected == CONNECTED ? TextClassifierService.this::onConnected
+ : TextClassifierService.this::onDisconnected);
+ }
+ };
+
+ @Nullable
+ @Override
+ public final IBinder onBind(@NonNull Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mBinder;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean onUnbind(@NonNull Intent intent) {
+ onDisconnected();
+ return super.onUnbind(intent);
+ }
+
+ /**
+ * Called when the Android system connects to service.
+ */
+ public void onConnected() {
+ }
+
+ /**
+ * Called when the Android system disconnects from the service.
+ *
+ * <p> At this point this service may no longer be an active {@link TextClassifierService}.
+ */
+ public void onDisconnected() {
+ }
+
+ /**
+ * Returns suggested text selection start and end indices, recognized entity types, and their
+ * associated confidence scores. The entity types are ordered from highest to lowest scoring.
+ *
+ * @param sessionId the session id
+ * @param request the text selection request
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ @MainThread
+ public abstract void onSuggestSelection(
+ @Nullable TextClassificationSessionId sessionId,
+ @NonNull TextSelection.Request request,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextSelection> callback);
+
+ /**
+ * Classifies the specified text and returns a {@link TextClassification} object that can be
+ * used to generate a widget for handling the classified text.
+ *
+ * @param sessionId the session id
+ * @param request the text classification request
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ @MainThread
+ public abstract void onClassifyText(
+ @Nullable TextClassificationSessionId sessionId,
+ @NonNull TextClassification.Request request,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextClassification> callback);
+
+ /**
+ * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
+ * links information.
+ *
+ * @param sessionId the session id
+ * @param request the text classification request
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ @MainThread
+ public abstract void onGenerateLinks(
+ @Nullable TextClassificationSessionId sessionId,
+ @NonNull TextLinks.Request request,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextLinks> callback);
+
+ /**
+ * Detects and returns the language of the give text.
+ *
+ * @param sessionId the session id
+ * @param request the language detection request
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ @MainThread
+ public void onDetectLanguage(
+ @Nullable TextClassificationSessionId sessionId,
+ @NonNull TextLanguage.Request request,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextLanguage> callback) {
+ mSingleThreadExecutor.submit(() ->
+ callback.onSuccess(getLocalTextClassifier().detectLanguage(request)));
+ }
+
+ /**
+ * Suggests and returns a list of actions according to the given conversation.
+ *
+ * @param sessionId the session id
+ * @param request the conversation actions request
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ @MainThread
+ public void onSuggestConversationActions(
+ @Nullable TextClassificationSessionId sessionId,
+ @NonNull ConversationActions.Request request,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<ConversationActions> callback) {
+ mSingleThreadExecutor.submit(() ->
+ callback.onSuccess(getLocalTextClassifier().suggestConversationActions(request)));
+ }
+
+ /**
+ * Writes the selection event.
+ * This is called when a selection event occurs. e.g. user changed selection; or smart selection
+ * happened.
+ *
+ * <p>The default implementation ignores the event.
+ *
+ * @param sessionId the session id
+ * @param event the selection event
+ * @deprecated
+ * Use {@link #onTextClassifierEvent(TextClassificationSessionId, TextClassifierEvent)}
+ * instead
+ */
+ @Deprecated
+ @MainThread
+ public void onSelectionEvent(
+ @Nullable TextClassificationSessionId sessionId, @NonNull SelectionEvent event) {}
+
+ /**
+ * Writes the TextClassifier event.
+ * This is called when a TextClassifier event occurs. e.g. user changed selection,
+ * smart selection happened, or a link was clicked.
+ *
+ * <p>The default implementation ignores the event.
+ *
+ * @param sessionId the session id
+ * @param event the TextClassifier event
+ */
+ @MainThread
+ public void onTextClassifierEvent(
+ @Nullable TextClassificationSessionId sessionId, @NonNull TextClassifierEvent event) {}
+
+ /**
+ * Creates a new text classification session for the specified context.
+ *
+ * @param context the text classification context
+ * @param sessionId the session's Id
+ */
+ @MainThread
+ public void onCreateTextClassificationSession(
+ @NonNull TextClassificationContext context,
+ @NonNull TextClassificationSessionId sessionId) {}
+
+ /**
+ * Destroys the text classification session identified by the specified sessionId.
+ *
+ * @param sessionId the id of the session to destroy
+ */
+ @MainThread
+ public void onDestroyTextClassificationSession(
+ @NonNull TextClassificationSessionId sessionId) {}
+
+ /**
+ * Returns a TextClassifier that runs in this service's process.
+ * If the local TextClassifier is disabled, this returns {@link TextClassifier#NO_OP}.
+ *
+ * @deprecated Use {@link #getDefaultTextClassifierImplementation(Context)} instead.
+ */
+ @Deprecated
+ public final TextClassifier getLocalTextClassifier() {
+ return TextClassifier.NO_OP;
+ }
+
+ /**
+ * Returns the platform's default TextClassifier implementation.
+ *
+ * @throws RuntimeException if the TextClassifier from
+ * PackageManager#getDefaultTextClassifierPackageName() calls
+ * this method.
+ */
+ @NonNull
+ public static TextClassifier getDefaultTextClassifierImplementation(@NonNull Context context) {
+ final String defaultTextClassifierPackageName =
+ context.getPackageManager().getDefaultTextClassifierPackageName();
+ if (TextUtils.isEmpty(defaultTextClassifierPackageName)) {
+ return TextClassifier.NO_OP;
+ }
+ if (defaultTextClassifierPackageName.equals(context.getPackageName())) {
+ throw new RuntimeException(
+ "The default text classifier itself should not call the"
+ + "getDefaultTextClassifierImplementation() method.");
+ }
+ final TextClassificationManager tcm =
+ context.getSystemService(TextClassificationManager.class);
+ return tcm.getTextClassifier(TextClassifier.DEFAULT_SYSTEM);
+ }
+
+ /** @hide **/
+ public static <T extends Parcelable> T getResponse(Bundle bundle) {
+ return bundle.getParcelable(KEY_RESULT);
+ }
+
+ /** @hide **/
+ public static <T extends Parcelable> void putResponse(Bundle bundle, T response) {
+ bundle.putParcelable(KEY_RESULT, response);
+ }
+
+ /**
+ * Callbacks for TextClassifierService results.
+ *
+ * @param <T> the type of the result
+ */
+ public interface Callback<T> {
+ /**
+ * Returns the result.
+ */
+ void onSuccess(T result);
+
+ /**
+ * Signals a failure.
+ */
+ void onFailure(@NonNull CharSequence error);
+ }
+
+ /**
+ * Returns the component name of the textclassifier service from the given package.
+ * Otherwise, returns null.
+ *
+ * @param context
+ * @param packageName the package to look for.
+ * @param resolveFlags the flags that are used by PackageManager to resolve the component name.
+ * @hide
+ */
+ @Nullable
+ public static ComponentName getServiceComponentName(
+ Context context, String packageName, int resolveFlags) {
+ final Intent intent = new Intent(SERVICE_INTERFACE).setPackage(packageName);
+
+ final ResolveInfo ri = context.getPackageManager().resolveService(intent, resolveFlags);
+
+ if ((ri == null) || (ri.serviceInfo == null)) {
+ Slog.w(LOG_TAG, String.format("Package or service not found in package %s for user %d",
+ packageName, context.getUserId()));
+ return null;
+ }
+
+ final ServiceInfo si = ri.serviceInfo;
+
+ final String permission = si.permission;
+ if (Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE.equals(permission)) {
+ return si.getComponentName();
+ }
+ Slog.w(LOG_TAG, String.format(
+ "Service %s should require %s permission. Found %s permission",
+ si.getComponentName(),
+ Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE,
+ si.permission));
+ return null;
+ }
+
+ /**
+ * Forwards the callback result to a wrapped binder callback.
+ */
+ private static final class ProxyCallback<T extends Parcelable> implements Callback<T> {
+ private ITextClassifierCallback mTextClassifierCallback;
+
+ private ProxyCallback(ITextClassifierCallback textClassifierCallback) {
+ mTextClassifierCallback = Preconditions.checkNotNull(textClassifierCallback);
+ }
+
+ @Override
+ public void onSuccess(T result) {
+ try {
+ Bundle bundle = new Bundle(1);
+ bundle.putParcelable(KEY_RESULT, result);
+ mTextClassifierCallback.onSuccess(bundle);
+ } catch (RemoteException e) {
+ Slog.d(LOG_TAG, "Error calling callback");
+ }
+ }
+
+ @Override
+ public void onFailure(CharSequence error) {
+ try {
+ Slog.w(LOG_TAG, "Request fail: " + error);
+ mTextClassifierCallback.onFailure();
+ } catch (RemoteException e) {
+ Slog.d(LOG_TAG, "Error calling callback");
+ }
+ }
+ }
+}
diff --git a/android/service/textservice/SpellCheckerService.java b/android/service/textservice/SpellCheckerService.java
new file mode 100644
index 0000000..bd1b44c
--- /dev/null
+++ b/android/service/textservice/SpellCheckerService.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2011 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.textservice;
+
+import com.android.internal.textservice.ISpellCheckerService;
+import com.android.internal.textservice.ISpellCheckerServiceCallback;
+import com.android.internal.textservice.ISpellCheckerSession;
+import com.android.internal.textservice.ISpellCheckerSessionListener;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.text.method.WordIterator;
+import android.util.Log;
+import android.view.textservice.SentenceSuggestionsInfo;
+import android.view.textservice.SuggestionsInfo;
+import android.view.textservice.TextInfo;
+
+import java.lang.ref.WeakReference;
+import java.text.BreakIterator;
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * SpellCheckerService provides an abstract base class for a spell checker.
+ * This class combines a service to the system with the spell checker service interface that
+ * spell checker must implement.
+ *
+ * <p>In addition to the normal Service lifecycle methods, this class
+ * introduces a new specific callback that subclasses should override
+ * {@link #createSession()} to provide a spell checker session that is corresponding
+ * to requested language and so on. The spell checker session returned by this method
+ * should extend {@link SpellCheckerService.Session}.
+ * </p>
+ *
+ * <h3>Returning spell check results</h3>
+ *
+ * <p>{@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
+ * should return spell check results.
+ * It receives {@link android.view.textservice.TextInfo} and returns
+ * {@link android.view.textservice.SuggestionsInfo} for the input.
+ * You may want to override
+ * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} for
+ * better performance and quality.
+ * </p>
+ *
+ * <p>Please note that {@link SpellCheckerService.Session#getLocale()} does not return a valid
+ * locale before {@link SpellCheckerService.Session#onCreate()} </p>
+ *
+ */
+public abstract class SpellCheckerService extends Service {
+ private static final String TAG = SpellCheckerService.class.getSimpleName();
+ private static final boolean DBG = false;
+ public static final String SERVICE_INTERFACE =
+ "android.service.textservice.SpellCheckerService";
+
+ private final SpellCheckerServiceBinder mBinder = new SpellCheckerServiceBinder(this);
+
+
+ /**
+ * Implement to return the implementation of the internal spell checker
+ * service interface. Subclasses should not override.
+ */
+ @Override
+ public final IBinder onBind(final Intent intent) {
+ if (DBG) {
+ Log.w(TAG, "onBind");
+ }
+ return mBinder;
+ }
+
+ /**
+ * Factory method to create a spell checker session impl
+ * @return SpellCheckerSessionImpl which should be overridden by a concrete implementation.
+ */
+ public abstract Session createSession();
+
+ /**
+ * This abstract class should be overridden by a concrete implementation of a spell checker.
+ */
+ public static abstract class Session {
+ private InternalISpellCheckerSession mInternalSession;
+ private volatile SentenceLevelAdapter mSentenceLevelAdapter;
+
+ /**
+ * @hide
+ */
+ public final void setInternalISpellCheckerSession(InternalISpellCheckerSession session) {
+ mInternalSession = session;
+ }
+
+ /**
+ * This is called after the class is initialized, at which point it knows it can call
+ * getLocale() etc...
+ */
+ public abstract void onCreate();
+
+ /**
+ * Get suggestions for specified text in TextInfo.
+ * This function will run on the incoming IPC thread.
+ * So, this is not called on the main thread,
+ * but will be called in series on another thread.
+ * @param textInfo the text metadata
+ * @param suggestionsLimit the maximum number of suggestions to be returned
+ * @return SuggestionsInfo which contains suggestions for textInfo
+ */
+ public abstract SuggestionsInfo onGetSuggestions(TextInfo textInfo, int suggestionsLimit);
+
+ /**
+ * A batch process of onGetSuggestions.
+ * This function will run on the incoming IPC thread.
+ * So, this is not called on the main thread,
+ * but will be called in series on another thread.
+ * @param textInfos an array of the text metadata
+ * @param suggestionsLimit the maximum number of suggestions to be returned
+ * @param sequentialWords true if textInfos can be treated as sequential words.
+ * @return an array of {@link SentenceSuggestionsInfo} returned by
+ * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
+ */
+ public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
+ int suggestionsLimit, boolean sequentialWords) {
+ final int length = textInfos.length;
+ final SuggestionsInfo[] retval = new SuggestionsInfo[length];
+ for (int i = 0; i < length; ++i) {
+ retval[i] = onGetSuggestions(textInfos[i], suggestionsLimit);
+ retval[i].setCookieAndSequence(
+ textInfos[i].getCookie(), textInfos[i].getSequence());
+ }
+ return retval;
+ }
+
+ /**
+ * Get sentence suggestions for specified texts in an array of TextInfo.
+ * The default implementation splits the input text to words and returns
+ * {@link SentenceSuggestionsInfo} which contains suggestions for each word.
+ * This function will run on the incoming IPC thread.
+ * So, this is not called on the main thread,
+ * but will be called in series on another thread.
+ * When you override this method, make sure that suggestionsLimit is applied to suggestions
+ * that share the same start position and length.
+ * @param textInfos an array of the text metadata
+ * @param suggestionsLimit the maximum number of suggestions to be returned
+ * @return an array of {@link SentenceSuggestionsInfo} returned by
+ * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
+ */
+ public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos,
+ int suggestionsLimit) {
+ if (textInfos == null || textInfos.length == 0) {
+ return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS;
+ }
+ if (DBG) {
+ Log.d(TAG, "onGetSentenceSuggestionsMultiple: + " + textInfos.length + ", "
+ + suggestionsLimit);
+ }
+ if (mSentenceLevelAdapter == null) {
+ synchronized(this) {
+ if (mSentenceLevelAdapter == null) {
+ final String localeStr = getLocale();
+ if (!TextUtils.isEmpty(localeStr)) {
+ mSentenceLevelAdapter = new SentenceLevelAdapter(new Locale(localeStr));
+ }
+ }
+ }
+ }
+ if (mSentenceLevelAdapter == null) {
+ return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS;
+ }
+ final int infosSize = textInfos.length;
+ final SentenceSuggestionsInfo[] retval = new SentenceSuggestionsInfo[infosSize];
+ for (int i = 0; i < infosSize; ++i) {
+ final SentenceLevelAdapter.SentenceTextInfoParams textInfoParams =
+ mSentenceLevelAdapter.getSplitWords(textInfos[i]);
+ final ArrayList<SentenceLevelAdapter.SentenceWordItem> mItems =
+ textInfoParams.mItems;
+ final int itemsSize = mItems.size();
+ final TextInfo[] splitTextInfos = new TextInfo[itemsSize];
+ for (int j = 0; j < itemsSize; ++j) {
+ splitTextInfos[j] = mItems.get(j).mTextInfo;
+ }
+ retval[i] = SentenceLevelAdapter.reconstructSuggestions(
+ textInfoParams, onGetSuggestionsMultiple(
+ splitTextInfos, suggestionsLimit, true));
+ }
+ return retval;
+ }
+
+ /**
+ * Request to abort all tasks executed in SpellChecker.
+ * This function will run on the incoming IPC thread.
+ * So, this is not called on the main thread,
+ * but will be called in series on another thread.
+ */
+ public void onCancel() {}
+
+ /**
+ * Request to close this session.
+ * This function will run on the incoming IPC thread.
+ * So, this is not called on the main thread,
+ * but will be called in series on another thread.
+ */
+ public void onClose() {}
+
+ /**
+ * @return Locale for this session
+ */
+ public String getLocale() {
+ return mInternalSession.getLocale();
+ }
+
+ /**
+ * @return Bundle for this session
+ */
+ public Bundle getBundle() {
+ return mInternalSession.getBundle();
+ }
+ }
+
+ // Preventing from exposing ISpellCheckerSession.aidl, create an internal class.
+ private static class InternalISpellCheckerSession extends ISpellCheckerSession.Stub {
+ private ISpellCheckerSessionListener mListener;
+ private final Session mSession;
+ private final String mLocale;
+ private final Bundle mBundle;
+
+ public InternalISpellCheckerSession(String locale, ISpellCheckerSessionListener listener,
+ Bundle bundle, Session session) {
+ mListener = listener;
+ mSession = session;
+ mLocale = locale;
+ mBundle = bundle;
+ session.setInternalISpellCheckerSession(this);
+ }
+
+ @Override
+ public void onGetSuggestionsMultiple(
+ TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
+ int pri = Process.getThreadPriority(Process.myTid());
+ try {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ mListener.onGetSuggestions(
+ mSession.onGetSuggestionsMultiple(
+ textInfos, suggestionsLimit, sequentialWords));
+ } catch (RemoteException e) {
+ } finally {
+ Process.setThreadPriority(pri);
+ }
+ }
+
+ @Override
+ public void onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) {
+ try {
+ mListener.onGetSentenceSuggestions(
+ mSession.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit));
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public void onCancel() {
+ int pri = Process.getThreadPriority(Process.myTid());
+ try {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ mSession.onCancel();
+ } finally {
+ Process.setThreadPriority(pri);
+ }
+ }
+
+ @Override
+ public void onClose() {
+ int pri = Process.getThreadPriority(Process.myTid());
+ try {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ mSession.onClose();
+ } finally {
+ Process.setThreadPriority(pri);
+ mListener = null;
+ }
+ }
+
+ public String getLocale() {
+ return mLocale;
+ }
+
+ public Bundle getBundle() {
+ return mBundle;
+ }
+ }
+
+ private static class SpellCheckerServiceBinder extends ISpellCheckerService.Stub {
+ private final WeakReference<SpellCheckerService> mInternalServiceRef;
+
+ public SpellCheckerServiceBinder(SpellCheckerService service) {
+ mInternalServiceRef = new WeakReference<SpellCheckerService>(service);
+ }
+
+ /**
+ * Called from the system when an application is requesting a new spell checker session.
+ *
+ * <p>Note: This is an internal protocol used by the system to establish spell checker
+ * sessions, which is not guaranteed to be stable and is subject to change.</p>
+ *
+ * @param locale locale to be returned from {@link Session#getLocale()}
+ * @param listener IPC channel object to be used to implement
+ * {@link Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} and
+ * {@link Session#onGetSuggestions(TextInfo, int)}
+ * @param bundle bundle to be returned from {@link Session#getBundle()}
+ * @param callback IPC channel to return the result to the caller in an asynchronous manner
+ */
+ @Override
+ public void getISpellCheckerSession(
+ String locale, ISpellCheckerSessionListener listener, Bundle bundle,
+ ISpellCheckerServiceCallback callback) {
+ final SpellCheckerService service = mInternalServiceRef.get();
+ final InternalISpellCheckerSession internalSession;
+ if (service == null) {
+ // If the owner SpellCheckerService object was already destroyed and got GC-ed,
+ // the weak-reference returns null and we should just ignore this request.
+ internalSession = null;
+ } else {
+ final Session session = service.createSession();
+ internalSession =
+ new InternalISpellCheckerSession(locale, listener, bundle, session);
+ session.onCreate();
+ }
+ try {
+ callback.onSessionCreated(internalSession);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /**
+ * Adapter class to accommodate word level spell checking APIs to sentence level spell checking
+ * APIs used in
+ * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)}
+ */
+ private static class SentenceLevelAdapter {
+ public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS =
+ new SentenceSuggestionsInfo[] {};
+ private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null);
+ /**
+ * Container for split TextInfo parameters
+ */
+ public static class SentenceWordItem {
+ public final TextInfo mTextInfo;
+ public final int mStart;
+ public final int mLength;
+ public SentenceWordItem(TextInfo ti, int start, int end) {
+ mTextInfo = ti;
+ mStart = start;
+ mLength = end - start;
+ }
+ }
+
+ /**
+ * Container for originally queried TextInfo and parameters
+ */
+ public static class SentenceTextInfoParams {
+ final TextInfo mOriginalTextInfo;
+ final ArrayList<SentenceWordItem> mItems;
+ final int mSize;
+ public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) {
+ mOriginalTextInfo = ti;
+ mItems = items;
+ mSize = items.size();
+ }
+ }
+
+ private final WordIterator mWordIterator;
+ public SentenceLevelAdapter(Locale locale) {
+ mWordIterator = new WordIterator(locale);
+ }
+
+ private SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) {
+ final WordIterator wordIterator = mWordIterator;
+ final CharSequence originalText = originalTextInfo.getText();
+ final int cookie = originalTextInfo.getCookie();
+ final int start = 0;
+ final int end = originalText.length();
+ final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>();
+ wordIterator.setCharSequence(originalText, 0, originalText.length());
+ int wordEnd = wordIterator.following(start);
+ int wordStart = wordIterator.getBeginning(wordEnd);
+ if (DBG) {
+ Log.d(TAG, "iterator: break: ---- 1st word start = " + wordStart + ", end = "
+ + wordEnd + "\n" + originalText);
+ }
+ while (wordStart <= end && wordEnd != BreakIterator.DONE
+ && wordStart != BreakIterator.DONE) {
+ if (wordEnd >= start && wordEnd > wordStart) {
+ final CharSequence query = originalText.subSequence(wordStart, wordEnd);
+ final TextInfo ti = new TextInfo(query, 0, query.length(), cookie,
+ query.hashCode());
+ wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd));
+ if (DBG) {
+ Log.d(TAG, "Adapter: word (" + (wordItems.size() - 1) + ") " + query);
+ }
+ }
+ wordEnd = wordIterator.following(wordEnd);
+ if (wordEnd == BreakIterator.DONE) {
+ break;
+ }
+ wordStart = wordIterator.getBeginning(wordEnd);
+ }
+ return new SentenceTextInfoParams(originalTextInfo, wordItems);
+ }
+
+ public static SentenceSuggestionsInfo reconstructSuggestions(
+ SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) {
+ if (results == null || results.length == 0) {
+ return null;
+ }
+ if (DBG) {
+ Log.w(TAG, "Adapter: onGetSuggestions: got " + results.length);
+ }
+ if (originalTextInfoParams == null) {
+ if (DBG) {
+ Log.w(TAG, "Adapter: originalTextInfoParams is null.");
+ }
+ return null;
+ }
+ final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie();
+ final int originalSequence =
+ originalTextInfoParams.mOriginalTextInfo.getSequence();
+
+ final int querySize = originalTextInfoParams.mSize;
+ final int[] offsets = new int[querySize];
+ final int[] lengths = new int[querySize];
+ final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize];
+ for (int i = 0; i < querySize; ++i) {
+ final SentenceWordItem item = originalTextInfoParams.mItems.get(i);
+ SuggestionsInfo result = null;
+ for (int j = 0; j < results.length; ++j) {
+ final SuggestionsInfo cur = results[j];
+ if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) {
+ result = cur;
+ result.setCookieAndSequence(originalCookie, originalSequence);
+ break;
+ }
+ }
+ offsets[i] = item.mStart;
+ lengths[i] = item.mLength;
+ reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO;
+ if (DBG) {
+ final int size = reconstructedSuggestions[i].getSuggestionsCount();
+ Log.w(TAG, "reconstructedSuggestions(" + i + ")" + size + ", first = "
+ + (size > 0 ? reconstructedSuggestions[i].getSuggestionAt(0)
+ : "<none>") + ", offset = " + offsets[i] + ", length = "
+ + lengths[i]);
+ }
+ }
+ return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths);
+ }
+ }
+}
diff --git a/android/service/trust/TrustAgentService.java b/android/service/trust/TrustAgentService.java
new file mode 100644
index 0000000..61277e2
--- /dev/null
+++ b/android/service/trust/TrustAgentService.java
@@ -0,0 +1,672 @@
+/**
+ * Copyright (C) 2014 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.trust;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.util.Slog;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * A service that notifies the system about whether it believes the environment of the device
+ * to be trusted.
+ *
+ * <p>Trust agents may only be provided by the platform. It is expected that there is only
+ * one trust agent installed on the platform. In the event there is more than one,
+ * either trust agent can enable trust.
+ * </p>
+ *
+ * <p>To extend this class, you must declare the service in your manifest file with
+ * the {@link android.Manifest.permission#BIND_TRUST_AGENT} permission
+ * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
+ * <pre>
+ * <service android:name=".TrustAgent"
+ * android:label="@string/service_name"
+ * android:permission="android.permission.BIND_TRUST_AGENT">
+ * <intent-filter>
+ * <action android:name="android.service.trust.TrustAgentService" />
+ * </intent-filter>
+ * <meta-data android:name="android.service.trust.trustagent"
+ * android:value="@xml/trust_agent" />
+ * </service></pre>
+ *
+ * <p>The associated meta-data file can specify an activity that is accessible through Settings
+ * and should allow configuring the trust agent, as defined in
+ * {@link android.R.styleable#TrustAgent}. For example:</p>
+ *
+ * <pre>
+ * <trust-agent xmlns:android="http://schemas.android.com/apk/res/android"
+ * android:settingsActivity=".TrustAgentSettings" /></pre>
+ *
+ * @hide
+ */
+@SystemApi
+public class TrustAgentService extends Service {
+
+ private final String TAG = TrustAgentService.class.getSimpleName() +
+ "[" + getClass().getSimpleName() + "]";
+ private static final boolean DEBUG = false;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE
+ = "android.service.trust.TrustAgentService";
+
+ /**
+ * The name of the {@code meta-data} tag pointing to additional configuration of the trust
+ * agent.
+ */
+ public static final String TRUST_AGENT_META_DATA = "android.service.trust.trustagent";
+
+
+ /**
+ * Flag for {@link #grantTrust(CharSequence, long, int)} indicating that trust is being granted
+ * as the direct result of user action - such as solving a security challenge. The hint is used
+ * by the system to optimize the experience. Behavior may vary by device and release, so
+ * one should only set this parameter if it meets the above criteria rather than relying on
+ * the behavior of any particular device or release.
+ */
+ public static final int FLAG_GRANT_TRUST_INITIATED_BY_USER = 1 << 0;
+
+ /**
+ * Flag for {@link #grantTrust(CharSequence, long, int)} indicating that the agent would like
+ * to dismiss the keyguard. When using this flag, the {@code TrustAgentService} must ensure
+ * it is only set in response to a direct user action with the expectation of dismissing the
+ * keyguard.
+ */
+ public static final int FLAG_GRANT_TRUST_DISMISS_KEYGUARD = 1 << 1;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "FLAG_GRANT_TRUST_" }, value = {
+ FLAG_GRANT_TRUST_INITIATED_BY_USER,
+ FLAG_GRANT_TRUST_DISMISS_KEYGUARD,
+ })
+ public @interface GrantTrustFlags {}
+
+
+ /**
+ * Int enum indicating that escrow token is active.
+ * See {@link #onEscrowTokenStateReceived(long, int)}
+ *
+ */
+ public static final int TOKEN_STATE_ACTIVE = 1;
+
+ /**
+ * Int enum indicating that escow token is inactive.
+ * See {@link #onEscrowTokenStateReceived(long, int)}
+ *
+ */
+ public static final int TOKEN_STATE_INACTIVE = 0;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "TOKEN_STATE_" }, value = {
+ TOKEN_STATE_ACTIVE,
+ TOKEN_STATE_INACTIVE,
+ })
+ public @interface TokenState {}
+
+ private static final int MSG_UNLOCK_ATTEMPT = 1;
+ private static final int MSG_CONFIGURE = 2;
+ private static final int MSG_TRUST_TIMEOUT = 3;
+ private static final int MSG_DEVICE_LOCKED = 4;
+ private static final int MSG_DEVICE_UNLOCKED = 5;
+ private static final int MSG_UNLOCK_LOCKOUT = 6;
+ private static final int MSG_ESCROW_TOKEN_ADDED = 7;
+ private static final int MSG_ESCROW_TOKEN_STATE_RECEIVED = 8;
+ private static final int MSG_ESCROW_TOKEN_REMOVED = 9;
+
+ private static final String EXTRA_TOKEN = "token";
+ private static final String EXTRA_TOKEN_HANDLE = "token_handle";
+ private static final String EXTRA_USER_HANDLE = "user_handle";
+ private static final String EXTRA_TOKEN_STATE = "token_state";
+ private static final String EXTRA_TOKEN_REMOVED_RESULT = "token_removed_result";
+ /**
+ * Class containing raw data for a given configuration request.
+ */
+ private static final class ConfigurationData {
+ final IBinder token;
+ final List<PersistableBundle> options;
+ ConfigurationData(List<PersistableBundle> opts, IBinder t) {
+ options = opts;
+ token = t;
+ }
+ }
+
+ private ITrustAgentServiceCallback mCallback;
+
+ private Runnable mPendingGrantTrustTask;
+
+ private boolean mManagingTrust;
+
+ // Lock used to access mPendingGrantTrustTask and mCallback.
+ private final Object mLock = new Object();
+
+ private Handler mHandler = new Handler() {
+ public void handleMessage(android.os.Message msg) {
+ switch (msg.what) {
+ case MSG_UNLOCK_ATTEMPT:
+ onUnlockAttempt(msg.arg1 != 0);
+ break;
+ case MSG_UNLOCK_LOCKOUT:
+ onDeviceUnlockLockout(msg.arg1);
+ break;
+ case MSG_CONFIGURE: {
+ ConfigurationData data = (ConfigurationData) msg.obj;
+ boolean result = onConfigure(data.options);
+ if (data.token != null) {
+ try {
+ synchronized (mLock) {
+ mCallback.onConfigureCompleted(result, data.token);
+ }
+ } catch (RemoteException e) {
+ onError("calling onSetTrustAgentFeaturesEnabledCompleted()");
+ }
+ }
+ break;
+ }
+ case MSG_TRUST_TIMEOUT:
+ onTrustTimeout();
+ break;
+ case MSG_DEVICE_LOCKED:
+ onDeviceLocked();
+ break;
+ case MSG_DEVICE_UNLOCKED:
+ onDeviceUnlocked();
+ break;
+ case MSG_ESCROW_TOKEN_ADDED: {
+ Bundle data = msg.getData();
+ byte[] token = data.getByteArray(EXTRA_TOKEN);
+ long handle = data.getLong(EXTRA_TOKEN_HANDLE);
+ UserHandle user = (UserHandle) data.getParcelable(EXTRA_USER_HANDLE);
+ onEscrowTokenAdded(token, handle, user);
+ break;
+ }
+ case MSG_ESCROW_TOKEN_STATE_RECEIVED: {
+ Bundle data = msg.getData();
+ long handle = data.getLong(EXTRA_TOKEN_HANDLE);
+ int tokenState = data.getInt(EXTRA_TOKEN_STATE, TOKEN_STATE_INACTIVE);
+ onEscrowTokenStateReceived(handle, tokenState);
+ break;
+ }
+ case MSG_ESCROW_TOKEN_REMOVED: {
+ Bundle data = msg.getData();
+ long handle = data.getLong(EXTRA_TOKEN_HANDLE);
+ boolean success = data.getBoolean(EXTRA_TOKEN_REMOVED_RESULT);
+ onEscrowTokenRemoved(handle, success);
+ break;
+ }
+ }
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ ComponentName component = new ComponentName(this, getClass());
+ try {
+ ServiceInfo serviceInfo = getPackageManager().getServiceInfo(component, 0 /* flags */);
+ if (!Manifest.permission.BIND_TRUST_AGENT.equals(serviceInfo.permission)) {
+ throw new IllegalStateException(component.flattenToShortString()
+ + " is not declared with the permission "
+ + "\"" + Manifest.permission.BIND_TRUST_AGENT + "\"");
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Can't get ServiceInfo for " + component.toShortString());
+ }
+ }
+
+ /**
+ * Called after the user attempts to authenticate in keyguard with their device credentials,
+ * such as pin, pattern or password.
+ *
+ * @param successful true if the user successfully completed the challenge.
+ */
+ public void onUnlockAttempt(boolean successful) {
+ }
+
+ /**
+ * Called when the timeout provided by the agent expires. Note that this may be called earlier
+ * than requested by the agent if the trust timeout is adjusted by the system or
+ * {@link DevicePolicyManager}. The agent is expected to re-evaluate the trust state and only
+ * call {@link #grantTrust(CharSequence, long, boolean)} if the trust state should be
+ * continued.
+ */
+ public void onTrustTimeout() {
+ }
+
+ /**
+ * Called when the device enters a state where a PIN, pattern or
+ * password must be entered to unlock it.
+ */
+ public void onDeviceLocked() {
+ }
+
+ /**
+ * Called when the device leaves a state where a PIN, pattern or
+ * password must be entered to unlock it.
+ */
+ public void onDeviceUnlocked() {
+ }
+
+ /**
+ * Called when the device enters a temporary unlock lockout.
+ *
+ * <p>This occurs when the user has consecutively failed to unlock the device too many times,
+ * and must wait until a timeout has passed to perform another attempt. The user may then only
+ * use strong authentication mechanisms (PIN, pattern or password) to unlock the device.
+ * Calls to {@link #grantTrust(CharSequence, long, int)} will be ignored until the user has
+ * unlocked the device and {@link #onDeviceUnlocked()} is called.
+ *
+ * @param timeoutMs The amount of time, in milliseconds, that needs to elapse before the user
+ * can attempt to unlock the device again.
+ */
+ public void onDeviceUnlockLockout(long timeoutMs) {
+ }
+
+ /**
+ * Called when an escrow token is added for user userId.
+ *
+ * @param token the added token
+ * @param handle the handle to the corresponding internal synthetic password. A user is unlocked
+ * by presenting both handle and escrow token.
+ * @param user the user to which the escrow token is added.
+ *
+ */
+ public void onEscrowTokenAdded(byte[] token, long handle, UserHandle user) {
+ }
+
+ /**
+ * Called when an escrow token state is received upon request.
+ *
+ * @param handle the handle to the internal synthetic password.
+ * @param state the state of the requested escrow token, see {@link TokenState}.
+ *
+ */
+ public void onEscrowTokenStateReceived(long handle, @TokenState int tokenState) {
+ }
+
+ /**
+ * Called when an escrow token is removed.
+ *
+ * @param handle the handle to the removed the synthetic password.
+ * @param successful whether the removing operaiton is achieved.
+ *
+ */
+ public void onEscrowTokenRemoved(long handle, boolean successful) {
+ }
+
+ private void onError(String msg) {
+ Slog.v(TAG, "Remote exception while " + msg);
+ }
+
+ /**
+ * Called when device policy admin wants to enable specific options for agent in response to
+ * {@link DevicePolicyManager#setKeyguardDisabledFeatures(ComponentName, int)} and
+ * {@link DevicePolicyManager#setTrustAgentConfiguration(ComponentName, ComponentName,
+ * PersistableBundle)}.
+ * <p>Agents that support configuration options should overload this method and return 'true'.
+ *
+ * @param options The aggregated list of options or an empty list if no restrictions apply.
+ * @return true if it supports configuration options.
+ */
+ public boolean onConfigure(List<PersistableBundle> options) {
+ return false;
+ }
+
+ /**
+ * Call to grant trust on the device.
+ *
+ * @param message describes why the device is trusted, e.g. "Trusted by location".
+ * @param durationMs amount of time in milliseconds to keep the device in a trusted state.
+ * Trust for this agent will automatically be revoked when the timeout expires unless
+ * extended by a subsequent call to this function. The timeout is measured from the
+ * invocation of this function as dictated by {@link SystemClock#elapsedRealtime())}.
+ * For security reasons, the value should be no larger than necessary.
+ * The value may be adjusted by the system as necessary to comply with a policy controlled
+ * by the system or {@link DevicePolicyManager} restrictions. See {@link #onTrustTimeout()}
+ * for determining when trust expires.
+ * @param initiatedByUser this is a hint to the system that trust is being granted as the
+ * direct result of user action - such as solving a security challenge. The hint is used
+ * by the system to optimize the experience. Behavior may vary by device and release, so
+ * one should only set this parameter if it meets the above criteria rather than relying on
+ * the behavior of any particular device or release. Corresponds to
+ * {@link #FLAG_GRANT_TRUST_INITIATED_BY_USER}.
+ * @throws IllegalStateException if the agent is not currently managing trust.
+ *
+ * @deprecated use {@link #grantTrust(CharSequence, long, int)} instead.
+ */
+ @Deprecated
+ public final void grantTrust(
+ final CharSequence message, final long durationMs, final boolean initiatedByUser) {
+ grantTrust(message, durationMs, initiatedByUser ? FLAG_GRANT_TRUST_INITIATED_BY_USER : 0);
+ }
+
+ /**
+ * Call to grant trust on the device.
+ *
+ * @param message describes why the device is trusted, e.g. "Trusted by location".
+ * @param durationMs amount of time in milliseconds to keep the device in a trusted state.
+ * Trust for this agent will automatically be revoked when the timeout expires unless
+ * extended by a subsequent call to this function. The timeout is measured from the
+ * invocation of this function as dictated by {@link SystemClock#elapsedRealtime())}.
+ * For security reasons, the value should be no larger than necessary.
+ * The value may be adjusted by the system as necessary to comply with a policy controlled
+ * by the system or {@link DevicePolicyManager} restrictions. See {@link #onTrustTimeout()}
+ * for determining when trust expires.
+ * @param flags TBDocumented
+ * @throws IllegalStateException if the agent is not currently managing trust.
+ */
+ public final void grantTrust(
+ final CharSequence message, final long durationMs, @GrantTrustFlags final int flags) {
+ synchronized (mLock) {
+ if (!mManagingTrust) {
+ throw new IllegalStateException("Cannot grant trust if agent is not managing trust."
+ + " Call setManagingTrust(true) first.");
+ }
+ if (mCallback != null) {
+ try {
+ mCallback.grantTrust(message.toString(), durationMs, flags);
+ } catch (RemoteException e) {
+ onError("calling enableTrust()");
+ }
+ } else {
+ // Remember trust has been granted so we can effectively grant it once the service
+ // is bound.
+ mPendingGrantTrustTask = new Runnable() {
+ @Override
+ public void run() {
+ grantTrust(message, durationMs, flags);
+ }
+ };
+ }
+ }
+ }
+
+ /**
+ * Call to revoke trust on the device.
+ */
+ public final void revokeTrust() {
+ synchronized (mLock) {
+ if (mPendingGrantTrustTask != null) {
+ mPendingGrantTrustTask = null;
+ }
+ if (mCallback != null) {
+ try {
+ mCallback.revokeTrust();
+ } catch (RemoteException e) {
+ onError("calling revokeTrust()");
+ }
+ }
+ }
+ }
+
+ /**
+ * Call to notify the system if the agent is ready to manage trust.
+ *
+ * This property is not persistent across recreating the service and defaults to false.
+ * Therefore this method is typically called when initializing the agent in {@link #onCreate}.
+ *
+ * @param managingTrust indicates if the agent would like to manage trust.
+ */
+ public final void setManagingTrust(boolean managingTrust) {
+ synchronized (mLock) {
+ if (mManagingTrust != managingTrust) {
+ mManagingTrust = managingTrust;
+ if (mCallback != null) {
+ try {
+ mCallback.setManagingTrust(managingTrust);
+ } catch (RemoteException e) {
+ onError("calling setManagingTrust()");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Call to add an escrow token to derive a synthetic password. A synthetic password is an
+ * alternaive to the user-set password/pin/pattern in order to unlock encrypted disk. An escrow
+ * token can be taken and internally derive the synthetic password. The new added token will not
+ * be acivated until the user input the correct PIN/Passcode/Password once.
+ *
+ * Result will be return by callback {@link #onEscrowTokenAdded(long, int)}
+ *
+ * @param token an escrow token of high entropy.
+ * @param user the user which the escrow token will be added to.
+ *
+ */
+ public final void addEscrowToken(byte[] token, UserHandle user) {
+ synchronized (mLock) {
+ if (mCallback == null) {
+ Slog.w(TAG, "Cannot add escrow token if the agent is not connecting to framework");
+ throw new IllegalStateException("Trust agent is not connected");
+ }
+ try {
+ mCallback.addEscrowToken(token, user.getIdentifier());
+ } catch (RemoteException e) {
+ onError("calling addEscrowToken");
+ }
+ }
+ }
+
+ /**
+ * Call to check the active state of an escrow token.
+ *
+ * Result will be return in callback {@link #onEscrowTokenStateReceived(long, boolean)}
+ *
+ * @param handle the handle of escrow token to the internal synthetic password.
+ * @param user the user which the escrow token is added to.
+ *
+ */
+ public final void isEscrowTokenActive(long handle, UserHandle user) {
+ synchronized (mLock) {
+ if (mCallback == null) {
+ Slog.w(TAG, "Cannot add escrow token if the agent is not connecting to framework");
+ throw new IllegalStateException("Trust agent is not connected");
+ }
+ try {
+ mCallback.isEscrowTokenActive(handle, user.getIdentifier());
+ } catch (RemoteException e) {
+ onError("calling isEscrowTokenActive");
+ }
+ }
+ }
+
+ /**
+ * Call to remove the escrow token.
+ *
+ * Result will be return in callback {@link #onEscrowTokenRemoved(long, boolean)}
+ *
+ * @param handle the handle of escrow tokent to the internal synthetic password.
+ * @param user the user id which the escrow token is added to.
+ *
+ */
+ public final void removeEscrowToken(long handle, UserHandle user) {
+ synchronized (mLock) {
+ if (mCallback == null) {
+ Slog.w(TAG, "Cannot add escrow token if the agent is not connecting to framework");
+ throw new IllegalStateException("Trust agent is not connected");
+ }
+ try {
+ mCallback.removeEscrowToken(handle, user.getIdentifier());
+ } catch (RemoteException e) {
+ onError("callling removeEscrowToken");
+ }
+ }
+ }
+
+ /**
+ * Call to unlock user's FBE.
+ *
+ * @param handle the handle of escrow tokent to the internal synthetic password.
+ * @param token the escrow token
+ * @param user the user about to be unlocked.
+ *
+ */
+ public final void unlockUserWithToken(long handle, byte[] token, UserHandle user) {
+ UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
+ if (um.isUserUnlocked(user)) {
+ Slog.i(TAG, "User already unlocked");
+ return;
+ }
+
+ synchronized (mLock) {
+ if (mCallback == null) {
+ Slog.w(TAG, "Cannot add escrow token if the agent is not connecting to framework");
+ throw new IllegalStateException("Trust agent is not connected");
+ }
+ try {
+ mCallback.unlockUserWithToken(handle, token, user.getIdentifier());
+ } catch (RemoteException e) {
+ onError("calling unlockUserWithToken");
+ }
+ }
+ }
+
+ /**
+ * Request showing a transient error message on the keyguard.
+ * The message will be visible on the lock screen or always on display if possible but can be
+ * overridden by other keyguard events of higher priority - eg. fingerprint auth error.
+ * Other trust agents may override your message if posted simultaneously.
+ *
+ * @param message Message to show.
+ */
+ public final void showKeyguardErrorMessage(@NonNull CharSequence message) {
+ if (message == null) {
+ throw new IllegalArgumentException("message cannot be null");
+ }
+ synchronized (mLock) {
+ if (mCallback == null) {
+ Slog.w(TAG, "Cannot show message because service is not connected to framework.");
+ throw new IllegalStateException("Trust agent is not connected");
+ }
+ try {
+ mCallback.showKeyguardErrorMessage(message);
+ } catch (RemoteException e) {
+ onError("calling showKeyguardErrorMessage");
+ }
+ }
+ }
+
+ @Override
+ public final IBinder onBind(Intent intent) {
+ if (DEBUG) Slog.v(TAG, "onBind() intent = " + intent);
+ return new TrustAgentServiceWrapper();
+ }
+
+ private final class TrustAgentServiceWrapper extends ITrustAgentService.Stub {
+ @Override /* Binder API */
+ public void onUnlockAttempt(boolean successful) {
+ mHandler.obtainMessage(MSG_UNLOCK_ATTEMPT, successful ? 1 : 0, 0).sendToTarget();
+ }
+
+ @Override
+ public void onUnlockLockout(int timeoutMs) {
+ mHandler.obtainMessage(MSG_UNLOCK_LOCKOUT, timeoutMs, 0).sendToTarget();
+ }
+
+ @Override /* Binder API */
+ public void onTrustTimeout() {
+ mHandler.sendEmptyMessage(MSG_TRUST_TIMEOUT);
+ }
+
+ @Override /* Binder API */
+ public void onConfigure(List<PersistableBundle> args, IBinder token) {
+ mHandler.obtainMessage(MSG_CONFIGURE, new ConfigurationData(args, token))
+ .sendToTarget();
+ }
+
+ @Override
+ public void onDeviceLocked() throws RemoteException {
+ mHandler.obtainMessage(MSG_DEVICE_LOCKED).sendToTarget();
+ }
+
+ @Override
+ public void onDeviceUnlocked() throws RemoteException {
+ mHandler.obtainMessage(MSG_DEVICE_UNLOCKED).sendToTarget();
+ }
+
+ @Override /* Binder API */
+ public void setCallback(ITrustAgentServiceCallback callback) {
+ synchronized (mLock) {
+ mCallback = callback;
+ // The managingTrust property is false implicitly on the server-side, so we only
+ // need to set it here if the agent has decided to manage trust.
+ if (mManagingTrust) {
+ try {
+ mCallback.setManagingTrust(mManagingTrust);
+ } catch (RemoteException e ) {
+ onError("calling setManagingTrust()");
+ }
+ }
+ if (mPendingGrantTrustTask != null) {
+ mPendingGrantTrustTask.run();
+ mPendingGrantTrustTask = null;
+ }
+ }
+ }
+
+ @Override
+ public void onEscrowTokenAdded(byte[] token, long handle, UserHandle user) {
+ Message msg = mHandler.obtainMessage(MSG_ESCROW_TOKEN_ADDED);
+ msg.getData().putByteArray(EXTRA_TOKEN, token);
+ msg.getData().putLong(EXTRA_TOKEN_HANDLE, handle);
+ msg.getData().putParcelable(EXTRA_USER_HANDLE, user);
+ msg.sendToTarget();
+ }
+
+ public void onTokenStateReceived(long handle, int tokenState) {
+ Message msg = mHandler.obtainMessage(MSG_ESCROW_TOKEN_STATE_RECEIVED);
+ msg.getData().putLong(EXTRA_TOKEN_HANDLE, handle);
+ msg.getData().putInt(EXTRA_TOKEN_STATE, tokenState);
+ msg.sendToTarget();
+ }
+
+ public void onEscrowTokenRemoved(long handle, boolean successful) {
+ Message msg = mHandler.obtainMessage(MSG_ESCROW_TOKEN_REMOVED);
+ msg.getData().putLong(EXTRA_TOKEN_HANDLE, handle);
+ msg.getData().putBoolean(EXTRA_TOKEN_REMOVED_RESULT, successful);
+ msg.sendToTarget();
+ }
+ }
+}
diff --git a/android/service/voice/AlwaysOnHotwordDetector.java b/android/service/voice/AlwaysOnHotwordDetector.java
new file mode 100644
index 0000000..6f94112
--- /dev/null
+++ b/android/service/voice/AlwaysOnHotwordDetector.java
@@ -0,0 +1,1008 @@
+/**
+ * Copyright (C) 2014 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.voice;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.soundtrigger.IRecognitionStatusCallback;
+import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
+import android.hardware.soundtrigger.KeyphraseMetadata;
+import android.hardware.soundtrigger.SoundTrigger;
+import android.hardware.soundtrigger.SoundTrigger.ConfidenceLevel;
+import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
+import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra;
+import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
+import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
+import android.media.AudioFormat;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.internal.app.IVoiceInteractionManagerService;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+
+/**
+ * A class that lets a VoiceInteractionService implementation interact with
+ * always-on keyphrase detection APIs.
+ */
+public class AlwaysOnHotwordDetector {
+ //---- States of Keyphrase availability. Return codes for onAvailabilityChanged() ----//
+ /**
+ * Indicates that this hotword detector is no longer valid for any recognition
+ * and should not be used anymore.
+ */
+ private static final int STATE_INVALID = -3;
+
+ /**
+ * Indicates that recognition for the given keyphrase is not available on the system
+ * because of the hardware configuration.
+ * No further interaction should be performed with the detector that returns this availability.
+ */
+ public static final int STATE_HARDWARE_UNAVAILABLE = -2;
+ /**
+ * Indicates that recognition for the given keyphrase is not supported.
+ * No further interaction should be performed with the detector that returns this availability.
+ *
+ * @deprecated This is no longer a valid state. Enrollment can occur outside of
+ * {@link KeyphraseEnrollmentInfo} through another privileged application. We can no longer
+ * determine ahead of time if the keyphrase and locale are unsupported by the system.
+ */
+ @Deprecated
+ public static final int STATE_KEYPHRASE_UNSUPPORTED = -1;
+ /**
+ * Indicates that the given keyphrase is not enrolled.
+ * The caller may choose to begin an enrollment flow for the keyphrase.
+ */
+ public static final int STATE_KEYPHRASE_UNENROLLED = 1;
+ /**
+ * Indicates that the given keyphrase is currently enrolled and it's possible to start
+ * recognition for it.
+ */
+ public static final int STATE_KEYPHRASE_ENROLLED = 2;
+
+ /**
+ * Indicates that the detector isn't ready currently.
+ */
+ private static final int STATE_NOT_READY = 0;
+
+ //-- Flags for startRecognition ----//
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "RECOGNITION_FLAG_" }, value = {
+ RECOGNITION_FLAG_NONE,
+ RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO,
+ RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS,
+ RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION,
+ RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION,
+ })
+ public @interface RecognitionFlags {}
+
+ /**
+ * Empty flag for {@link #startRecognition(int)}.
+ *
+ * @hide
+ */
+ public static final int RECOGNITION_FLAG_NONE = 0;
+ /**
+ * Recognition flag for {@link #startRecognition(int)} that indicates
+ * whether the trigger audio for hotword needs to be captured.
+ */
+ public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 0x1;
+ /**
+ * Recognition flag for {@link #startRecognition(int)} that indicates
+ * whether the recognition should keep going on even after the keyphrase triggers.
+ * If this flag is specified, it's possible to get multiple triggers after a
+ * call to {@link #startRecognition(int)} if the user speaks the keyphrase multiple times.
+ * When this isn't specified, the default behavior is to stop recognition once the
+ * keyphrase is spoken, till the caller starts recognition again.
+ */
+ public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 0x2;
+
+ /**
+ * Audio capabilities flag for {@link #startRecognition(int)} that indicates
+ * if the underlying recognition should use AEC.
+ * This capability may or may not be supported by the system, and support can be queried
+ * by calling {@link #getSupportedAudioCapabilities()}. The corresponding capabilities field for
+ * this flag is {@link #AUDIO_CAPABILITY_ECHO_CANCELLATION}. If this flag is passed without the
+ * audio capability supported, there will be no audio effect applied.
+ */
+ public static final int RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION = 0x4;
+
+ /**
+ * Audio capabilities flag for {@link #startRecognition(int)} that indicates
+ * if the underlying recognition should use noise suppression.
+ * This capability may or may not be supported by the system, and support can be queried
+ * by calling {@link #getSupportedAudioCapabilities()}. The corresponding capabilities field for
+ * this flag is {@link #AUDIO_CAPABILITY_NOISE_SUPPRESSION}. If this flag is passed without the
+ * audio capability supported, there will be no audio effect applied.
+ */
+ public static final int RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION = 0x8;
+
+ //---- Recognition mode flags. Return codes for getSupportedRecognitionModes() ----//
+ // Must be kept in sync with the related attribute defined as searchKeyphraseRecognitionFlags.
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "RECOGNITION_MODE_" }, value = {
+ RECOGNITION_MODE_VOICE_TRIGGER,
+ RECOGNITION_MODE_USER_IDENTIFICATION,
+ })
+ public @interface RecognitionModes {}
+
+ /**
+ * Simple recognition of the key phrase.
+ * Returned by {@link #getSupportedRecognitionModes()}
+ */
+ public static final int RECOGNITION_MODE_VOICE_TRIGGER
+ = SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER;
+ /**
+ * User identification performed with the keyphrase recognition.
+ * Returned by {@link #getSupportedRecognitionModes()}
+ */
+ public static final int RECOGNITION_MODE_USER_IDENTIFICATION
+ = SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION;
+
+ //-- Audio capabilities. Values in returned bit field for getSupportedAudioCapabilities() --//
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "AUDIO_CAPABILITY_" }, value = {
+ AUDIO_CAPABILITY_ECHO_CANCELLATION,
+ AUDIO_CAPABILITY_NOISE_SUPPRESSION,
+ })
+ public @interface AudioCapabilities {}
+
+ /**
+ * If set the underlying module supports AEC.
+ * Returned by {@link #getSupportedAudioCapabilities()}
+ */
+ public static final int AUDIO_CAPABILITY_ECHO_CANCELLATION =
+ SoundTrigger.ModuleProperties.AUDIO_CAPABILITY_ECHO_CANCELLATION;
+
+ /**
+ * If set, the underlying module supports noise suppression.
+ * Returned by {@link #getSupportedAudioCapabilities()}
+ */
+ public static final int AUDIO_CAPABILITY_NOISE_SUPPRESSION =
+ SoundTrigger.ModuleProperties.AUDIO_CAPABILITY_NOISE_SUPPRESSION;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "MODEL_PARAM_" }, value = {
+ MODEL_PARAM_THRESHOLD_FACTOR,
+ })
+ public @interface ModelParams {}
+
+ /**
+ * Controls the sensitivity threshold adjustment factor for a given model.
+ * Negative value corresponds to less sensitive model (high threshold) and
+ * a positive value corresponds to a more sensitive model (low threshold).
+ * Default value is 0.
+ */
+ public static final int MODEL_PARAM_THRESHOLD_FACTOR =
+ android.hardware.soundtrigger.ModelParams.THRESHOLD_FACTOR;
+
+ static final String TAG = "AlwaysOnHotwordDetector";
+ static final boolean DBG = false;
+
+ private static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR;
+ private static final int STATUS_OK = SoundTrigger.STATUS_OK;
+
+ private static final int MSG_AVAILABILITY_CHANGED = 1;
+ private static final int MSG_HOTWORD_DETECTED = 2;
+ private static final int MSG_DETECTION_ERROR = 3;
+ private static final int MSG_DETECTION_PAUSE = 4;
+ private static final int MSG_DETECTION_RESUME = 5;
+
+ private final String mText;
+ private final Locale mLocale;
+ /**
+ * The metadata of the Keyphrase, derived from the enrollment application.
+ * This may be null if this keyphrase isn't supported by the enrollment application.
+ */
+ @Nullable
+ private KeyphraseMetadata mKeyphraseMetadata;
+ private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
+ private final IVoiceInteractionManagerService mModelManagementService;
+ private final SoundTriggerListener mInternalCallback;
+ private final Callback mExternalCallback;
+ private final Object mLock = new Object();
+ private final Handler mHandler;
+
+ private int mAvailability = STATE_NOT_READY;
+
+ /**
+ * A ModelParamRange is a representation of supported parameter range for a
+ * given loaded model.
+ */
+ public static final class ModelParamRange {
+ private final SoundTrigger.ModelParamRange mModelParamRange;
+
+ /** @hide */
+ ModelParamRange(SoundTrigger.ModelParamRange modelParamRange) {
+ mModelParamRange = modelParamRange;
+ }
+
+ /**
+ * Get the beginning of the param range
+ *
+ * @return The inclusive start of the supported range.
+ */
+ public int getStart() {
+ return mModelParamRange.getStart();
+ }
+
+ /**
+ * Get the end of the param range
+ *
+ * @return The inclusive end of the supported range.
+ */
+ public int getEnd() {
+ return mModelParamRange.getEnd();
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return mModelParamRange.toString();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ return mModelParamRange.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return mModelParamRange.hashCode();
+ }
+ }
+
+ /**
+ * Additional payload for {@link Callback#onDetected}.
+ */
+ public static class EventPayload {
+ private final boolean mTriggerAvailable;
+ // Indicates if {@code captureSession} can be used to continue capturing more audio
+ // from the DSP hardware.
+ private final boolean mCaptureAvailable;
+ // The session to use when attempting to capture more audio from the DSP hardware.
+ private final int mCaptureSession;
+ private final AudioFormat mAudioFormat;
+ // Raw data associated with the event.
+ // This is the audio that triggered the keyphrase if {@code isTriggerAudio} is true.
+ private final byte[] mData;
+
+ private EventPayload(boolean triggerAvailable, boolean captureAvailable,
+ AudioFormat audioFormat, int captureSession, byte[] data) {
+ mTriggerAvailable = triggerAvailable;
+ mCaptureAvailable = captureAvailable;
+ mCaptureSession = captureSession;
+ mAudioFormat = audioFormat;
+ mData = data;
+ }
+
+ /**
+ * Gets the format of the audio obtained using {@link #getTriggerAudio()}.
+ * May be null if there's no audio present.
+ */
+ @Nullable
+ public AudioFormat getCaptureAudioFormat() {
+ return mAudioFormat;
+ }
+
+ /**
+ * Gets the raw audio that triggered the keyphrase.
+ * This may be null if the trigger audio isn't available.
+ * If non-null, the format of the audio can be obtained by calling
+ * {@link #getCaptureAudioFormat()}.
+ *
+ * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
+ */
+ @Nullable
+ public byte[] getTriggerAudio() {
+ if (mTriggerAvailable) {
+ return mData;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the session ID to start a capture from the DSP.
+ * This may be null if streaming capture isn't possible.
+ * If non-null, the format of the audio that can be captured can be
+ * obtained using {@link #getCaptureAudioFormat()}.
+ *
+ * TODO: Candidate for Public API when the API to start capture with a session ID
+ * is made public.
+ *
+ * TODO: Add this to {@link #getCaptureAudioFormat()}:
+ * "Gets the format of the audio obtained using {@link #getTriggerAudio()}
+ * or {@link #getCaptureSession()}. May be null if no audio can be obtained
+ * for either the trigger or a streaming session."
+ *
+ * TODO: Should this return a known invalid value instead?
+ *
+ * @hide
+ */
+ @Nullable
+ @UnsupportedAppUsage
+ public Integer getCaptureSession() {
+ if (mCaptureAvailable) {
+ return mCaptureSession;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Callbacks for always-on hotword detection.
+ */
+ public static abstract class Callback {
+ /**
+ * Called when the hotword availability changes.
+ * This indicates a change in the availability of recognition for the given keyphrase.
+ * It's called at least once with the initial availability.<p/>
+ *
+ * Availability implies whether the hardware on this system is capable of listening for
+ * the given keyphrase or not. <p/>
+ *
+ * @see AlwaysOnHotwordDetector#STATE_HARDWARE_UNAVAILABLE
+ * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNSUPPORTED
+ * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNENROLLED
+ * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_ENROLLED
+ */
+ public abstract void onAvailabilityChanged(int status);
+ /**
+ * Called when the keyphrase is spoken.
+ * This implicitly stops listening for the keyphrase once it's detected.
+ * Clients should start a recognition again once they are done handling this
+ * detection.
+ *
+ * @param eventPayload Payload data for the detection event.
+ * This may contain the trigger audio, if requested when calling
+ * {@link AlwaysOnHotwordDetector#startRecognition(int)}.
+ */
+ public abstract void onDetected(@NonNull EventPayload eventPayload);
+ /**
+ * Called when the detection fails due to an error.
+ */
+ public abstract void onError();
+ /**
+ * Called when the recognition is paused temporarily for some reason.
+ * This is an informational callback, and the clients shouldn't be doing anything here
+ * except showing an indication on their UI if they have to.
+ */
+ public abstract void onRecognitionPaused();
+ /**
+ * Called when the recognition is resumed after it was temporarily paused.
+ * This is an informational callback, and the clients shouldn't be doing anything here
+ * except showing an indication on their UI if they have to.
+ */
+ public abstract void onRecognitionResumed();
+ }
+
+ /**
+ * @param text The keyphrase text to get the detector for.
+ * @param locale The java locale for the detector.
+ * @param callback A non-null Callback for receiving the recognition events.
+ * @param modelManagementService A service that allows management of sound models.
+ * @hide
+ */
+ public AlwaysOnHotwordDetector(String text, Locale locale, Callback callback,
+ KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
+ IVoiceInteractionManagerService modelManagementService) {
+ mText = text;
+ mLocale = locale;
+ mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
+ mExternalCallback = callback;
+ mHandler = new MyHandler();
+ mInternalCallback = new SoundTriggerListener(mHandler);
+ mModelManagementService = modelManagementService;
+ new RefreshAvailabiltyTask().execute();
+ }
+
+ /**
+ * Gets the recognition modes supported by the associated keyphrase.
+ *
+ * @see #RECOGNITION_MODE_USER_IDENTIFICATION
+ * @see #RECOGNITION_MODE_VOICE_TRIGGER
+ *
+ * @throws UnsupportedOperationException if the keyphrase itself isn't supported.
+ * Callers should only call this method after a supported state callback on
+ * {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
+ * @throws IllegalStateException if the detector is in an invalid state.
+ * This may happen if another detector has been instantiated or the
+ * {@link VoiceInteractionService} hosting this detector has been shut down.
+ */
+ public @RecognitionModes int getSupportedRecognitionModes() {
+ if (DBG) Slog.d(TAG, "getSupportedRecognitionModes()");
+ synchronized (mLock) {
+ return getSupportedRecognitionModesLocked();
+ }
+ }
+
+ private int getSupportedRecognitionModesLocked() {
+ if (mAvailability == STATE_INVALID) {
+ throw new IllegalStateException(
+ "getSupportedRecognitionModes called on an invalid detector");
+ }
+
+ // This method only makes sense if we can actually support a recognition.
+ if (mAvailability != STATE_KEYPHRASE_ENROLLED || mKeyphraseMetadata == null) {
+ throw new UnsupportedOperationException(
+ "Getting supported recognition modes for the keyphrase is not supported");
+ }
+
+ return mKeyphraseMetadata.getRecognitionModeFlags();
+ }
+
+ /**
+ * Get the audio capabilities supported by the platform which can be enabled when
+ * starting a recognition.
+ * Caller must be the active voice interaction service via
+ * Settings.Secure.VOICE_INTERACTION_SERVICE.
+ *
+ * @see #AUDIO_CAPABILITY_ECHO_CANCELLATION
+ * @see #AUDIO_CAPABILITY_NOISE_SUPPRESSION
+ *
+ * @return Bit field encoding of the AudioCapabilities supported.
+ */
+ @AudioCapabilities
+ public int getSupportedAudioCapabilities() {
+ if (DBG) Slog.d(TAG, "getSupportedAudioCapabilities()");
+ synchronized (mLock) {
+ return getSupportedAudioCapabilitiesLocked();
+ }
+ }
+
+ private int getSupportedAudioCapabilitiesLocked() {
+ try {
+ ModuleProperties properties =
+ mModelManagementService.getDspModuleProperties();
+ if (properties != null) {
+ return properties.getAudioCapabilities();
+ }
+
+ return 0;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Starts recognition for the associated keyphrase.
+ * Caller must be the active voice interaction service via
+ * Settings.Secure.VOICE_INTERACTION_SERVICE.
+ *
+ * @see #RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO
+ * @see #RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS
+ *
+ * @param recognitionFlags The flags to control the recognition properties.
+ * @return Indicates whether the call succeeded or not.
+ * @throws UnsupportedOperationException if the recognition isn't supported.
+ * Callers should only call this method after a supported state callback on
+ * {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
+ * @throws IllegalStateException if the detector is in an invalid state.
+ * This may happen if another detector has been instantiated or the
+ * {@link VoiceInteractionService} hosting this detector has been shut down.
+ */
+ public boolean startRecognition(@RecognitionFlags int recognitionFlags) {
+ if (DBG) Slog.d(TAG, "startRecognition(" + recognitionFlags + ")");
+ synchronized (mLock) {
+ if (mAvailability == STATE_INVALID) {
+ throw new IllegalStateException("startRecognition called on an invalid detector");
+ }
+
+ // Check if we can start/stop a recognition.
+ if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
+ throw new UnsupportedOperationException(
+ "Recognition for the given keyphrase is not supported");
+ }
+
+ return startRecognitionLocked(recognitionFlags) == STATUS_OK;
+ }
+ }
+
+ /**
+ * Stops recognition for the associated keyphrase.
+ * Caller must be the active voice interaction service via
+ * Settings.Secure.VOICE_INTERACTION_SERVICE.
+ *
+ * @return Indicates whether the call succeeded or not.
+ * @throws UnsupportedOperationException if the recognition isn't supported.
+ * Callers should only call this method after a supported state callback on
+ * {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
+ * @throws IllegalStateException if the detector is in an invalid state.
+ * This may happen if another detector has been instantiated or the
+ * {@link VoiceInteractionService} hosting this detector has been shut down.
+ */
+ public boolean stopRecognition() {
+ if (DBG) Slog.d(TAG, "stopRecognition()");
+ synchronized (mLock) {
+ if (mAvailability == STATE_INVALID) {
+ throw new IllegalStateException("stopRecognition called on an invalid detector");
+ }
+
+ // Check if we can start/stop a recognition.
+ if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
+ throw new UnsupportedOperationException(
+ "Recognition for the given keyphrase is not supported");
+ }
+
+ return stopRecognitionLocked() == STATUS_OK;
+ }
+ }
+
+ /**
+ * Set a model specific {@link ModelParams} with the given value. This
+ * parameter will keep its value for the duration the model is loaded regardless of starting and
+ * stopping recognition. Once the model is unloaded, the value will be lost.
+ * {@link AlwaysOnHotwordDetector#queryParameter} should be checked first before calling this
+ * method.
+ * Caller must be the active voice interaction service via
+ * Settings.Secure.VOICE_INTERACTION_SERVICE.
+ *
+ * @param modelParam {@link ModelParams}
+ * @param value Value to set
+ * @return - {@link SoundTrigger#STATUS_OK} in case of success
+ * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached
+ * - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter
+ * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or
+ * if API is not supported by HAL
+ */
+ public int setParameter(@ModelParams int modelParam, int value) {
+ if (DBG) {
+ Slog.d(TAG, "setParameter(" + modelParam + ", " + value + ")");
+ }
+
+ synchronized (mLock) {
+ if (mAvailability == STATE_INVALID) {
+ throw new IllegalStateException("setParameter called on an invalid detector");
+ }
+
+ return setParameterLocked(modelParam, value);
+ }
+ }
+
+ /**
+ * Get a model specific {@link ModelParams}. This parameter will keep its value
+ * for the duration the model is loaded regardless of starting and stopping recognition.
+ * Once the model is unloaded, the value will be lost. If the value is not set, a default
+ * value is returned. See {@link ModelParams} for parameter default values.
+ * {@link AlwaysOnHotwordDetector#queryParameter} should be checked first before
+ * calling this method.
+ * Caller must be the active voice interaction service via
+ * Settings.Secure.VOICE_INTERACTION_SERVICE.
+ *
+ * @param modelParam {@link ModelParams}
+ * @return value of parameter
+ */
+ public int getParameter(@ModelParams int modelParam) {
+ if (DBG) {
+ Slog.d(TAG, "getParameter(" + modelParam + ")");
+ }
+
+ synchronized (mLock) {
+ if (mAvailability == STATE_INVALID) {
+ throw new IllegalStateException("getParameter called on an invalid detector");
+ }
+
+ return getParameterLocked(modelParam);
+ }
+ }
+
+ /**
+ * Determine if parameter control is supported for the given model handle.
+ * This method should be checked prior to calling {@link AlwaysOnHotwordDetector#setParameter}
+ * or {@link AlwaysOnHotwordDetector#getParameter}.
+ * Caller must be the active voice interaction service via
+ * Settings.Secure.VOICE_INTERACTION_SERVICE.
+ *
+ * @param modelParam {@link ModelParams}
+ * @return supported range of parameter, null if not supported
+ */
+ @Nullable
+ public ModelParamRange queryParameter(@ModelParams int modelParam) {
+ if (DBG) {
+ Slog.d(TAG, "queryParameter(" + modelParam + ")");
+ }
+
+ synchronized (mLock) {
+ if (mAvailability == STATE_INVALID) {
+ throw new IllegalStateException("queryParameter called on an invalid detector");
+ }
+
+ return queryParameterLocked(modelParam);
+ }
+ }
+
+ /**
+ * Creates an intent to start the enrollment for the associated keyphrase.
+ * This intent must be invoked using {@link Context#startForegroundService(Intent)}.
+ * Starting re-enrollment is only valid if the keyphrase is un-enrolled,
+ * i.e. {@link #STATE_KEYPHRASE_UNENROLLED},
+ * otherwise {@link #createReEnrollIntent()} should be preferred.
+ *
+ * @return An {@link Intent} to start enrollment for the given keyphrase.
+ * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
+ * Callers should only call this method after a supported state callback on
+ * {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
+ * @throws IllegalStateException if the detector is in an invalid state.
+ * This may happen if another detector has been instantiated or the
+ * {@link VoiceInteractionService} hosting this detector has been shut down.
+ */
+ public Intent createEnrollIntent() {
+ if (DBG) Slog.d(TAG, "createEnrollIntent");
+ synchronized (mLock) {
+ return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_ENROLL);
+ }
+ }
+
+ /**
+ * Creates an intent to start the un-enrollment for the associated keyphrase.
+ * This intent must be invoked using {@link Context#startForegroundService(Intent)}.
+ * Starting re-enrollment is only valid if the keyphrase is already enrolled,
+ * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error.
+ *
+ * @return An {@link Intent} to start un-enrollment for the given keyphrase.
+ * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
+ * Callers should only call this method after a supported state callback on
+ * {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
+ * @throws IllegalStateException if the detector is in an invalid state.
+ * This may happen if another detector has been instantiated or the
+ * {@link VoiceInteractionService} hosting this detector has been shut down.
+ */
+ public Intent createUnEnrollIntent() {
+ if (DBG) Slog.d(TAG, "createUnEnrollIntent");
+ synchronized (mLock) {
+ return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_UN_ENROLL);
+ }
+ }
+
+ /**
+ * Creates an intent to start the re-enrollment for the associated keyphrase.
+ * This intent must be invoked using {@link Context#startForegroundService(Intent)}.
+ * Starting re-enrollment is only valid if the keyphrase is already enrolled,
+ * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error.
+ *
+ * @return An {@link Intent} to start re-enrollment for the given keyphrase.
+ * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
+ * Callers should only call this method after a supported state callback on
+ * {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
+ * @throws IllegalStateException if the detector is in an invalid state.
+ * This may happen if another detector has been instantiated or the
+ * {@link VoiceInteractionService} hosting this detector has been shut down.
+ */
+ public Intent createReEnrollIntent() {
+ if (DBG) Slog.d(TAG, "createReEnrollIntent");
+ synchronized (mLock) {
+ return getManageIntentLocked(KeyphraseEnrollmentInfo.MANAGE_ACTION_RE_ENROLL);
+ }
+ }
+
+ private Intent getManageIntentLocked(@KeyphraseEnrollmentInfo.ManageActions int action) {
+ if (mAvailability == STATE_INVALID) {
+ throw new IllegalStateException("getManageIntent called on an invalid detector");
+ }
+
+ // This method only makes sense if we can actually support a recognition.
+ if (mAvailability != STATE_KEYPHRASE_ENROLLED
+ && mAvailability != STATE_KEYPHRASE_UNENROLLED) {
+ throw new UnsupportedOperationException(
+ "Managing the given keyphrase is not supported");
+ }
+
+ return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale);
+ }
+
+ /**
+ * Invalidates this hotword detector so that any future calls to this result
+ * in an IllegalStateException.
+ *
+ * @hide
+ */
+ void invalidate() {
+ synchronized (mLock) {
+ mAvailability = STATE_INVALID;
+ notifyStateChangedLocked();
+ }
+ }
+
+ /**
+ * Reloads the sound models from the service.
+ *
+ * @hide
+ */
+ void onSoundModelsChanged() {
+ synchronized (mLock) {
+ if (mAvailability == STATE_INVALID
+ || mAvailability == STATE_HARDWARE_UNAVAILABLE) {
+ Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config");
+ return;
+ }
+
+ // Stop the recognition before proceeding.
+ // This is done because we want to stop the recognition on an older model if it changed
+ // or was deleted.
+ // The availability change callback should ensure that the client starts recognition
+ // again if needed.
+ if (mAvailability == STATE_KEYPHRASE_ENROLLED) {
+ stopRecognitionLocked();
+ }
+
+ // Execute a refresh availability task - which should then notify of a change.
+ new RefreshAvailabiltyTask().execute();
+ }
+ }
+
+ private int startRecognitionLocked(int recognitionFlags) {
+ KeyphraseRecognitionExtra[] recognitionExtra = new KeyphraseRecognitionExtra[1];
+ // TODO: Do we need to do something about the confidence level here?
+ recognitionExtra[0] = new KeyphraseRecognitionExtra(mKeyphraseMetadata.getId(),
+ mKeyphraseMetadata.getRecognitionModeFlags(), 0, new ConfidenceLevel[0]);
+ boolean captureTriggerAudio =
+ (recognitionFlags&RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO) != 0;
+ boolean allowMultipleTriggers =
+ (recognitionFlags&RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS) != 0;
+
+ int audioCapabilities = 0;
+ if ((recognitionFlags & RECOGNITION_FLAG_ENABLE_AUDIO_ECHO_CANCELLATION) != 0) {
+ audioCapabilities |= AUDIO_CAPABILITY_ECHO_CANCELLATION;
+ }
+ if ((recognitionFlags & RECOGNITION_FLAG_ENABLE_AUDIO_NOISE_SUPPRESSION) != 0) {
+ audioCapabilities |= AUDIO_CAPABILITY_NOISE_SUPPRESSION;
+ }
+
+ int code;
+ try {
+ code = mModelManagementService.startRecognition(
+ mKeyphraseMetadata.getId(), mLocale.toLanguageTag(), mInternalCallback,
+ new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
+ recognitionExtra, null /* additional data */, audioCapabilities));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ if (code != STATUS_OK) {
+ Slog.w(TAG, "startRecognition() failed with error code " + code);
+ }
+ return code;
+ }
+
+ private int stopRecognitionLocked() {
+ int code;
+ try {
+ code = mModelManagementService.stopRecognition(mKeyphraseMetadata.getId(),
+ mInternalCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ if (code != STATUS_OK) {
+ Slog.w(TAG, "stopRecognition() failed with error code " + code);
+ }
+ return code;
+ }
+
+ private int setParameterLocked(@ModelParams int modelParam, int value) {
+ try {
+ int code = mModelManagementService.setParameter(mKeyphraseMetadata.getId(), modelParam,
+ value);
+
+ if (code != STATUS_OK) {
+ Slog.w(TAG, "setParameter failed with error code " + code);
+ }
+
+ return code;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private int getParameterLocked(@ModelParams int modelParam) {
+ try {
+ return mModelManagementService.getParameter(mKeyphraseMetadata.getId(), modelParam);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Nullable
+ private ModelParamRange queryParameterLocked(@ModelParams int modelParam) {
+ try {
+ SoundTrigger.ModelParamRange modelParamRange =
+ mModelManagementService.queryParameter(mKeyphraseMetadata.getId(), modelParam);
+
+ if (modelParamRange == null) {
+ return null;
+ }
+
+ return new ModelParamRange(modelParamRange);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private void notifyStateChangedLocked() {
+ Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED);
+ message.arg1 = mAvailability;
+ message.sendToTarget();
+ }
+
+ /** @hide */
+ static final class SoundTriggerListener extends IRecognitionStatusCallback.Stub {
+ private final Handler mHandler;
+
+ public SoundTriggerListener(Handler handler) {
+ mHandler = handler;
+ }
+
+ @Override
+ public void onKeyphraseDetected(KeyphraseRecognitionEvent event) {
+ if (DBG) {
+ Slog.d(TAG, "onDetected(" + event + ")");
+ } else {
+ Slog.i(TAG, "onDetected");
+ }
+ Message.obtain(mHandler, MSG_HOTWORD_DETECTED,
+ new EventPayload(event.triggerInData, event.captureAvailable,
+ event.captureFormat, event.captureSession, event.data))
+ .sendToTarget();
+ }
+ @Override
+ public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
+ Slog.w(TAG, "Generic sound trigger event detected at AOHD: " + event);
+ }
+
+ @Override
+ public void onError(int status) {
+ Slog.i(TAG, "onError: " + status);
+ mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
+ }
+
+ @Override
+ public void onRecognitionPaused() {
+ Slog.i(TAG, "onRecognitionPaused");
+ mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE);
+ }
+
+ @Override
+ public void onRecognitionResumed() {
+ Slog.i(TAG, "onRecognitionResumed");
+ mHandler.sendEmptyMessage(MSG_DETECTION_RESUME);
+ }
+ }
+
+ class MyHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ synchronized (mLock) {
+ if (mAvailability == STATE_INVALID) {
+ Slog.w(TAG, "Received message: " + msg.what + " for an invalid detector");
+ return;
+ }
+ }
+
+ switch (msg.what) {
+ case MSG_AVAILABILITY_CHANGED:
+ mExternalCallback.onAvailabilityChanged(msg.arg1);
+ break;
+ case MSG_HOTWORD_DETECTED:
+ mExternalCallback.onDetected((EventPayload) msg.obj);
+ break;
+ case MSG_DETECTION_ERROR:
+ mExternalCallback.onError();
+ break;
+ case MSG_DETECTION_PAUSE:
+ mExternalCallback.onRecognitionPaused();
+ break;
+ case MSG_DETECTION_RESUME:
+ mExternalCallback.onRecognitionResumed();
+ break;
+ default:
+ super.handleMessage(msg);
+ }
+ }
+ }
+
+ class RefreshAvailabiltyTask extends AsyncTask<Void, Void, Void> {
+
+ @Override
+ public Void doInBackground(Void... params) {
+ int availability = internalGetInitialAvailability();
+
+ synchronized (mLock) {
+ if (availability == STATE_NOT_READY) {
+ internalUpdateEnrolledKeyphraseMetadata();
+ if (mKeyphraseMetadata != null) {
+ availability = STATE_KEYPHRASE_ENROLLED;
+ } else {
+ availability = STATE_KEYPHRASE_UNENROLLED;
+ }
+ }
+
+ if (DBG) {
+ Slog.d(TAG, "Hotword availability changed from " + mAvailability
+ + " -> " + availability);
+ }
+ mAvailability = availability;
+ notifyStateChangedLocked();
+ }
+ return null;
+ }
+
+ /**
+ * @return The initial availability without checking the enrollment status.
+ */
+ private int internalGetInitialAvailability() {
+ synchronized (mLock) {
+ // This detector has already been invalidated.
+ if (mAvailability == STATE_INVALID) {
+ return STATE_INVALID;
+ }
+ }
+
+ ModuleProperties dspModuleProperties;
+ try {
+ dspModuleProperties =
+ mModelManagementService.getDspModuleProperties();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ // No DSP available
+ if (dspModuleProperties == null) {
+ return STATE_HARDWARE_UNAVAILABLE;
+ }
+
+ return STATE_NOT_READY;
+ }
+
+ private void internalUpdateEnrolledKeyphraseMetadata() {
+ try {
+ mKeyphraseMetadata = mModelManagementService.getEnrolledKeyphraseMetadata(
+ mText, mLocale.toLanguageTag());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /** @hide */
+ public void dump(String prefix, PrintWriter pw) {
+ synchronized (mLock) {
+ pw.print(prefix); pw.print("Text="); pw.println(mText);
+ pw.print(prefix); pw.print("Locale="); pw.println(mLocale);
+ pw.print(prefix); pw.print("Availability="); pw.println(mAvailability);
+ pw.print(prefix); pw.print("KeyphraseMetadata="); pw.println(mKeyphraseMetadata);
+ pw.print(prefix); pw.print("EnrollmentInfo="); pw.println(mKeyphraseEnrollmentInfo);
+ }
+ }
+}
diff --git a/android/service/voice/VoiceInteractionManagerInternal.java b/android/service/voice/VoiceInteractionManagerInternal.java
new file mode 100644
index 0000000..b38067b
--- /dev/null
+++ b/android/service/voice/VoiceInteractionManagerInternal.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 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.voice;
+
+import android.os.Bundle;
+import android.os.IBinder;
+
+
+/**
+ * @hide
+ * Private interface to the VoiceInteractionManagerService for use by ActivityManagerService.
+ */
+public abstract class VoiceInteractionManagerInternal {
+
+ /**
+ * Start a new voice interaction session when requested from within an activity
+ * by Activity.startLocalVoiceInteraction()
+ * @param callingActivity The binder token representing the calling activity.
+ * @param options
+ */
+ public abstract void startLocalVoiceInteraction(IBinder callingActivity, Bundle options);
+
+ /**
+ * Returns whether the currently selected voice interaction service supports local voice
+ * interaction for launching from an Activity.
+ */
+ public abstract boolean supportsLocalVoiceInteraction();
+
+ public abstract void stopLocalVoiceInteraction(IBinder callingActivity);
+}
\ No newline at end of file
diff --git a/android/service/voice/VoiceInteractionService.java b/android/service/voice/VoiceInteractionService.java
new file mode 100644
index 0000000..45d3465
--- /dev/null
+++ b/android/service/voice/VoiceInteractionService.java
@@ -0,0 +1,419 @@
+/**
+ * Copyright (C) 2014 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.voice;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
+import android.media.voice.KeyphraseModelManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IVoiceActionCheckCallback;
+import com.android.internal.app.IVoiceInteractionManagerService;
+import com.android.internal.util.function.pooled.PooledLambda;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Top-level service of the current global voice interactor, which is providing
+ * support for hotwording, the back-end of a {@link android.app.VoiceInteractor}, etc.
+ * The current VoiceInteractionService that has been selected by the user is kept
+ * always running by the system, to allow it to do things like listen for hotwords
+ * in the background to instigate voice interactions.
+ *
+ * <p>Because this service is always running, it should be kept as lightweight as
+ * possible. Heavy-weight operations (including showing UI) should be implemented
+ * in the associated {@link android.service.voice.VoiceInteractionSessionService} when
+ * an actual voice interaction is taking place, and that service should run in a
+ * separate process from this one.
+ */
+public class VoiceInteractionService extends Service {
+ static final String TAG = VoiceInteractionService.class.getSimpleName();
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_VOICE_INTERACTION} permission so
+ * that other applications can not abuse it.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE =
+ "android.service.voice.VoiceInteractionService";
+
+ /**
+ * Name under which a VoiceInteractionService component publishes information about itself.
+ * This meta-data should reference an XML resource containing a
+ * <code><{@link
+ * android.R.styleable#VoiceInteractionService voice-interaction-service}></code> tag.
+ */
+ public static final String SERVICE_META_DATA = "android.voice_interaction";
+
+ IVoiceInteractionService mInterface = new IVoiceInteractionService.Stub() {
+ @Override
+ public void ready() {
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ VoiceInteractionService::onReady, VoiceInteractionService.this));
+ }
+
+ @Override
+ public void shutdown() {
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ VoiceInteractionService::onShutdownInternal, VoiceInteractionService.this));
+ }
+
+ @Override
+ public void soundModelsChanged() {
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ VoiceInteractionService::onSoundModelsChangedInternal,
+ VoiceInteractionService.this));
+ }
+
+ @Override
+ public void launchVoiceAssistFromKeyguard() {
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ VoiceInteractionService::onLaunchVoiceAssistFromKeyguard,
+ VoiceInteractionService.this));
+ }
+
+ @Override
+ public void getActiveServiceSupportedActions(List<String> voiceActions,
+ IVoiceActionCheckCallback callback) {
+ Handler.getMain().executeOrSendMessage(
+ PooledLambda.obtainMessage(VoiceInteractionService::onHandleVoiceActionCheck,
+ VoiceInteractionService.this,
+ voiceActions,
+ callback));
+ }
+ };
+
+ IVoiceInteractionManagerService mSystemService;
+
+ private final Object mLock = new Object();
+
+ private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
+
+ private AlwaysOnHotwordDetector mHotwordDetector;
+
+ /**
+ * Called when a user has activated an affordance to launch voice assist from the Keyguard.
+ *
+ * <p>This method will only be called if the VoiceInteractionService has set
+ * {@link android.R.attr#supportsLaunchVoiceAssistFromKeyguard} and the Keyguard is showing.</p>
+ *
+ * <p>A valid implementation must start a new activity that should use {@link
+ * android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} to display
+ * on top of the lock screen.</p>
+ */
+ public void onLaunchVoiceAssistFromKeyguard() {
+ }
+
+ /**
+ * Check whether the given service component is the currently active
+ * VoiceInteractionService.
+ */
+ public static boolean isActiveService(Context context, ComponentName service) {
+ String cur = Settings.Secure.getString(context.getContentResolver(),
+ Settings.Secure.VOICE_INTERACTION_SERVICE);
+ if (cur == null || cur.isEmpty()) {
+ return false;
+ }
+ ComponentName curComp = ComponentName.unflattenFromString(cur);
+ if (curComp == null) {
+ return false;
+ }
+ return curComp.equals(service);
+ }
+
+ /**
+ * Set contextual options you would always like to have disabled when a session
+ * is shown. The flags may be any combination of
+ * {@link VoiceInteractionSession#SHOW_WITH_ASSIST VoiceInteractionSession.SHOW_WITH_ASSIST} and
+ * {@link VoiceInteractionSession#SHOW_WITH_SCREENSHOT
+ * VoiceInteractionSession.SHOW_WITH_SCREENSHOT}.
+ */
+ public void setDisabledShowContext(int flags) {
+ try {
+ mSystemService.setDisabledShowContext(flags);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Return the value set by {@link #setDisabledShowContext}.
+ */
+ public int getDisabledShowContext() {
+ try {
+ return mSystemService.getDisabledShowContext();
+ } catch (RemoteException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Request that the associated {@link android.service.voice.VoiceInteractionSession} be
+ * shown to the user, starting it if necessary.
+ * @param args Arbitrary arguments that will be propagated to the session.
+ * @param flags Indicates additional optional behavior that should be performed. May
+ * be any combination of
+ * {@link VoiceInteractionSession#SHOW_WITH_ASSIST VoiceInteractionSession.SHOW_WITH_ASSIST} and
+ * {@link VoiceInteractionSession#SHOW_WITH_SCREENSHOT
+ * VoiceInteractionSession.SHOW_WITH_SCREENSHOT}
+ * to request that the system generate and deliver assist data on the current foreground
+ * app as part of showing the session UI.
+ */
+ public void showSession(Bundle args, int flags) {
+ if (mSystemService == null) {
+ throw new IllegalStateException("Not available until onReady() is called");
+ }
+ try {
+ mSystemService.showSession(args, flags);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Request to query for what extended voice actions this service supports. This method will
+ * be called when the system checks the supported actions of this
+ * {@link VoiceInteractionService}. Supported actions may be delivered to
+ * {@link VoiceInteractionSession} later to request a session to perform an action.
+ *
+ * <p>Voice actions are defined in support libraries and could vary based on platform context.
+ * For example, car related voice actions will be defined in car support libraries.
+ *
+ * @param voiceActions A set of checked voice actions.
+ * @return Returns a subset of checked voice actions. Additional voice actions in the
+ * returned set will be ignored. Returns empty set if no actions are supported.
+ */
+ @NonNull
+ public Set<String> onGetSupportedVoiceActions(@NonNull Set<String> voiceActions) {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mInterface.asBinder();
+ }
+ return null;
+ }
+
+ /**
+ * Called during service initialization to tell you when the system is ready
+ * to receive interaction from it. You should generally do initialization here
+ * rather than in {@link #onCreate}. Methods such as {@link #showSession} and
+ * {@link #createAlwaysOnHotwordDetector}
+ * will not be operational until this point.
+ */
+ public void onReady() {
+ mSystemService = IVoiceInteractionManagerService.Stub.asInterface(
+ ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
+ Objects.requireNonNull(mSystemService);
+ try {
+ mSystemService.asBinder().linkToDeath(mDeathRecipient, 0);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "unable to link to death with system service");
+ }
+ mKeyphraseEnrollmentInfo = new KeyphraseEnrollmentInfo(getPackageManager());
+ }
+
+ private IBinder.DeathRecipient mDeathRecipient = () -> {
+ Log.e(TAG, "system service binder died shutting down");
+ Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
+ VoiceInteractionService::onShutdownInternal, VoiceInteractionService.this));
+ };
+
+
+ private void onShutdownInternal() {
+ onShutdown();
+ // Stop any active recognitions when shutting down.
+ // This ensures that if implementations forget to stop any active recognition,
+ // It's still guaranteed to have been stopped.
+ // This helps with cases where the voice interaction implementation is changed
+ // by the user.
+ safelyShutdownHotwordDetector();
+ }
+
+ /**
+ * Called during service de-initialization to tell you when the system is shutting the
+ * service down.
+ * At this point this service may no longer be the active {@link VoiceInteractionService}.
+ */
+ public void onShutdown() {
+ }
+
+ private void onSoundModelsChangedInternal() {
+ synchronized (this) {
+ if (mHotwordDetector != null) {
+ // TODO: Stop recognition if a sound model that was being recognized gets deleted.
+ mHotwordDetector.onSoundModelsChanged();
+ }
+ }
+ }
+
+ private void onHandleVoiceActionCheck(List<String> voiceActions,
+ IVoiceActionCheckCallback callback) {
+ if (callback != null) {
+ try {
+ Set<String> voiceActionsSet = new ArraySet<>(voiceActions);
+ Set<String> resultSet = onGetSupportedVoiceActions(voiceActionsSet);
+ callback.onComplete(new ArrayList<>(resultSet));
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /**
+ * Creates an {@link AlwaysOnHotwordDetector} for the given keyphrase and locale.
+ * This instance must be retained and used by the client.
+ * Calling this a second time invalidates the previously created hotword detector
+ * which can no longer be used to manage recognition.
+ *
+ * @param keyphrase The keyphrase that's being used, for example "Hello Android".
+ * @param locale The locale for which the enrollment needs to be performed.
+ * @param callback The callback to notify of detection events.
+ * @return An always-on hotword detector for the given keyphrase and locale.
+ */
+ public final AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(
+ String keyphrase, Locale locale, AlwaysOnHotwordDetector.Callback callback) {
+ if (mSystemService == null) {
+ throw new IllegalStateException("Not available until onReady() is called");
+ }
+ synchronized (mLock) {
+ // Allow only one concurrent recognition via the APIs.
+ safelyShutdownHotwordDetector();
+ mHotwordDetector = new AlwaysOnHotwordDetector(keyphrase, locale, callback,
+ mKeyphraseEnrollmentInfo, mSystemService);
+ }
+ return mHotwordDetector;
+ }
+
+ /**
+ * Creates an {@link KeyphraseModelManager} to use for enrolling voice models outside of the
+ * pre-bundled system voice models.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES)
+ @NonNull
+ public final KeyphraseModelManager createKeyphraseModelManager() {
+ if (mSystemService == null) {
+ throw new IllegalStateException("Not available until onReady() is called");
+ }
+ synchronized (mLock) {
+ return new KeyphraseModelManager(mSystemService);
+ }
+ }
+
+ /**
+ * @return Details of keyphrases available for enrollment.
+ * @hide
+ */
+ @VisibleForTesting
+ protected final KeyphraseEnrollmentInfo getKeyphraseEnrollmentInfo() {
+ return mKeyphraseEnrollmentInfo;
+ }
+
+ /**
+ * Checks if a given keyphrase and locale are supported to create an
+ * {@link AlwaysOnHotwordDetector}.
+ *
+ * @return true if the keyphrase and locale combination is supported, false otherwise.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public final boolean isKeyphraseAndLocaleSupportedForHotword(String keyphrase, Locale locale) {
+ if (mKeyphraseEnrollmentInfo == null) {
+ return false;
+ }
+ return mKeyphraseEnrollmentInfo.getKeyphraseMetadata(keyphrase, locale) != null;
+ }
+
+ private void safelyShutdownHotwordDetector() {
+ synchronized (mLock) {
+ if (mHotwordDetector == null) {
+ return;
+ }
+
+ try {
+ mHotwordDetector.stopRecognition();
+ } catch (Exception ex) {
+ // Ignore.
+ }
+
+ try {
+ mHotwordDetector.invalidate();
+ } catch (Exception ex) {
+ // Ignore.
+ }
+
+ mHotwordDetector = null;
+ }
+ }
+
+ /**
+ * Provide hints to be reflected in the system UI.
+ *
+ * @param hints Arguments used to show UI.
+ */
+ public final void setUiHints(@NonNull Bundle hints) {
+ if (hints == null) {
+ throw new IllegalArgumentException("Hints must be non-null");
+ }
+
+ try {
+ mSystemService.setUiHints(hints);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("VOICE INTERACTION");
+ synchronized (mLock) {
+ pw.println(" AlwaysOnHotwordDetector");
+ if (mHotwordDetector == null) {
+ pw.println(" NULL");
+ } else {
+ mHotwordDetector.dump(" ", pw);
+ }
+ }
+ }
+}
diff --git a/android/service/voice/VoiceInteractionServiceInfo.java b/android/service/voice/VoiceInteractionServiceInfo.java
new file mode 100644
index 0000000..e1a9a05
--- /dev/null
+++ b/android/service/voice/VoiceInteractionServiceInfo.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2014 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.voice;
+
+import android.Manifest;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/** @hide */
+public class VoiceInteractionServiceInfo {
+ static final String TAG = "VoiceInteractionServiceInfo";
+
+ private String mParseError;
+
+ private ServiceInfo mServiceInfo;
+ private String mSessionService;
+ private String mRecognitionService;
+ private String mSettingsActivity;
+ private boolean mSupportsAssist;
+ private boolean mSupportsLaunchFromKeyguard;
+ private boolean mSupportsLocalInteraction;
+
+ public VoiceInteractionServiceInfo(PackageManager pm, ComponentName comp)
+ throws PackageManager.NameNotFoundException {
+ this(pm, pm.getServiceInfo(comp, PackageManager.GET_META_DATA));
+ }
+
+ public VoiceInteractionServiceInfo(PackageManager pm, ComponentName comp, int userHandle)
+ throws PackageManager.NameNotFoundException {
+ this(pm, getServiceInfoOrThrow(comp, userHandle));
+ }
+
+ static ServiceInfo getServiceInfoOrThrow(ComponentName comp, int userHandle)
+ throws PackageManager.NameNotFoundException {
+ try {
+ ServiceInfo si = AppGlobals.getPackageManager().getServiceInfo(comp,
+ PackageManager.GET_META_DATA
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
+ userHandle);
+ if (si != null) {
+ return si;
+ }
+ } catch (RemoteException e) {
+ }
+ throw new PackageManager.NameNotFoundException(comp.toString());
+ }
+
+ public VoiceInteractionServiceInfo(PackageManager pm, ServiceInfo si) {
+ if (si == null) {
+ mParseError = "Service not available";
+ return;
+ }
+ if (!Manifest.permission.BIND_VOICE_INTERACTION.equals(si.permission)) {
+ mParseError = "Service does not require permission "
+ + Manifest.permission.BIND_VOICE_INTERACTION;
+ return;
+ }
+
+ XmlResourceParser parser = null;
+ try {
+ parser = si.loadXmlMetaData(pm, VoiceInteractionService.SERVICE_META_DATA);
+ if (parser == null) {
+ mParseError = "No " + VoiceInteractionService.SERVICE_META_DATA
+ + " meta-data for " + si.packageName;
+ return;
+ }
+
+ Resources res = pm.getResourcesForApplication(si.applicationInfo);
+
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ }
+
+ String nodeName = parser.getName();
+ if (!"voice-interaction-service".equals(nodeName)) {
+ mParseError = "Meta-data does not start with voice-interaction-service tag";
+ return;
+ }
+
+ TypedArray array = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.VoiceInteractionService);
+ mSessionService = array.getString(
+ com.android.internal.R.styleable.VoiceInteractionService_sessionService);
+ mRecognitionService = array.getString(
+ com.android.internal.R.styleable.VoiceInteractionService_recognitionService);
+ mSettingsActivity = array.getString(
+ com.android.internal.R.styleable.VoiceInteractionService_settingsActivity);
+ mSupportsAssist = array.getBoolean(
+ com.android.internal.R.styleable.VoiceInteractionService_supportsAssist,
+ false);
+ mSupportsLaunchFromKeyguard = array.getBoolean(com.android.internal.
+ R.styleable.VoiceInteractionService_supportsLaunchVoiceAssistFromKeyguard,
+ false);
+ mSupportsLocalInteraction = array.getBoolean(com.android.internal.
+ R.styleable.VoiceInteractionService_supportsLocalInteraction, false);
+ array.recycle();
+ if (mSessionService == null) {
+ mParseError = "No sessionService specified";
+ return;
+ }
+ if (mRecognitionService == null) {
+ mParseError = "No recognitionService specified";
+ return;
+ }
+ } catch (XmlPullParserException e) {
+ mParseError = "Error parsing voice interation service meta-data: " + e;
+ Log.w(TAG, "error parsing voice interaction service meta-data", e);
+ return;
+ } catch (IOException e) {
+ mParseError = "Error parsing voice interation service meta-data: " + e;
+ Log.w(TAG, "error parsing voice interaction service meta-data", e);
+ return;
+ } catch (PackageManager.NameNotFoundException e) {
+ mParseError = "Error parsing voice interation service meta-data: " + e;
+ Log.w(TAG, "error parsing voice interaction service meta-data", e);
+ return;
+ } finally {
+ if (parser != null) parser.close();
+ }
+ mServiceInfo = si;
+ }
+
+ public String getParseError() {
+ return mParseError;
+ }
+
+ public ServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ public String getSessionService() {
+ return mSessionService;
+ }
+
+ public String getRecognitionService() {
+ return mRecognitionService;
+ }
+
+ public String getSettingsActivity() {
+ return mSettingsActivity;
+ }
+
+ public boolean getSupportsAssist() {
+ return mSupportsAssist;
+ }
+
+ public boolean getSupportsLaunchFromKeyguard() {
+ return mSupportsLaunchFromKeyguard;
+ }
+
+ public boolean getSupportsLocalInteraction() {
+ return mSupportsLocalInteraction;
+ }
+}
diff --git a/android/service/voice/VoiceInteractionSession.java b/android/service/voice/VoiceInteractionSession.java
new file mode 100644
index 0000000..4a0dd87
--- /dev/null
+++ b/android/service/voice/VoiceInteractionSession.java
@@ -0,0 +1,2121 @@
+/**
+ * Copyright (C) 2014 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.voice;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.DirectAction;
+import android.app.Instrumentation;
+import android.app.VoiceInteractor;
+import android.app.assist.AssistContent;
+import android.app.assist.AssistStructure;
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ParceledListSlice;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.inputmethodservice.SoftInputWindow;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.Message;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.DebugUtils;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+import com.android.internal.annotations.Immutable;
+import com.android.internal.app.IVoiceInteractionManagerService;
+import com.android.internal.app.IVoiceInteractionSessionShowCallback;
+import com.android.internal.app.IVoiceInteractor;
+import com.android.internal.app.IVoiceInteractorCallback;
+import com.android.internal.app.IVoiceInteractorRequest;
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.function.pooled.PooledLambda;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * An active voice interaction session, providing a facility for the implementation
+ * to interact with the user in the voice interaction layer. The user interface is
+ * initially shown by default, and can be created be overriding {@link #onCreateContentView()}
+ * in which the UI can be built.
+ *
+ * <p>A voice interaction session can be self-contained, ultimately calling {@link #finish}
+ * when done. It can also initiate voice interactions with applications by calling
+ * {@link #startVoiceActivity}</p>.
+ */
+public class VoiceInteractionSession implements KeyEvent.Callback, ComponentCallbacks2 {
+ static final String TAG = "VoiceInteractionSession";
+ static final boolean DEBUG = false;
+
+ /**
+ * Flag received in {@link #onShow}: originator requested that the session be started with
+ * assist data from the currently focused activity.
+ */
+ public static final int SHOW_WITH_ASSIST = 1<<0;
+
+ /**
+ * Flag received in {@link #onShow}: originator requested that the session be started with
+ * a screen shot of the currently focused activity.
+ */
+ public static final int SHOW_WITH_SCREENSHOT = 1<<1;
+
+ /**
+ * Flag for use with {@link #onShow}: indicates that the session has been started from the
+ * system assist gesture.
+ */
+ public static final int SHOW_SOURCE_ASSIST_GESTURE = 1<<2;
+
+ /**
+ * Flag for use with {@link #onShow}: indicates that the application itself has invoked
+ * the assistant.
+ */
+ public static final int SHOW_SOURCE_APPLICATION = 1<<3;
+
+ /**
+ * Flag for use with {@link #onShow}: indicates that an Activity has invoked the voice
+ * interaction service for a local interaction using
+ * {@link Activity#startLocalVoiceInteraction(Bundle)}.
+ */
+ public static final int SHOW_SOURCE_ACTIVITY = 1<<4;
+
+ /**
+ * Flag for use with {@link #onShow}: indicates that the voice interaction service was invoked
+ * from a physical button.
+ */
+ public static final int SHOW_SOURCE_PUSH_TO_TALK = 1 << 5;
+
+ /**
+ * Flag for use with {@link #onShow}: indicates that the voice interaction service was invoked
+ * from a notification.
+ */
+ public static final int SHOW_SOURCE_NOTIFICATION = 1 << 6;
+
+ /**
+ * Flag for use with {@link #onShow}: indicates that the voice interaction service was invoked
+ * from an Android automotive system UI.
+ */
+ public static final int SHOW_SOURCE_AUTOMOTIVE_SYSTEM_UI = 1 << 7;
+
+ final Context mContext;
+ final HandlerCaller mHandlerCaller;
+
+ final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
+
+ IVoiceInteractionManagerService mSystemService;
+ IBinder mToken;
+
+ int mTheme = 0;
+ LayoutInflater mInflater;
+ TypedArray mThemeAttrs;
+ View mRootView;
+ FrameLayout mContentFrame;
+ SoftInputWindow mWindow;
+
+ boolean mUiEnabled = true;
+ boolean mInitialized;
+ boolean mWindowAdded;
+ boolean mWindowVisible;
+ boolean mWindowWasVisible;
+ boolean mInShowWindow;
+
+ final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<IBinder, Request>();
+
+ final Insets mTmpInsets = new Insets();
+
+ final WeakReference<VoiceInteractionSession> mWeakRef
+ = new WeakReference<VoiceInteractionSession>(this);
+
+ // Registry of remote callbacks pending a reply with reply handles.
+ final Map<SafeResultListener, Consumer<Bundle>> mRemoteCallbacks = new ArrayMap<>();
+
+ ICancellationSignal mKillCallback;
+
+ final IVoiceInteractor mInteractor = new IVoiceInteractor.Stub() {
+ @Override
+ public IVoiceInteractorRequest startConfirmation(String callingPackage,
+ IVoiceInteractorCallback callback, VoiceInteractor.Prompt prompt, Bundle extras) {
+ ConfirmationRequest request = new ConfirmationRequest(callingPackage,
+ Binder.getCallingUid(), callback, VoiceInteractionSession.this,
+ prompt, extras);
+ addRequest(request);
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_START_CONFIRMATION,
+ request));
+ return request.mInterface;
+ }
+
+ @Override
+ public IVoiceInteractorRequest startPickOption(String callingPackage,
+ IVoiceInteractorCallback callback, VoiceInteractor.Prompt prompt,
+ VoiceInteractor.PickOptionRequest.Option[] options, Bundle extras) {
+ PickOptionRequest request = new PickOptionRequest(callingPackage,
+ Binder.getCallingUid(), callback, VoiceInteractionSession.this,
+ prompt, options, extras);
+ addRequest(request);
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_START_PICK_OPTION,
+ request));
+ return request.mInterface;
+ }
+
+ @Override
+ public IVoiceInteractorRequest startCompleteVoice(String callingPackage,
+ IVoiceInteractorCallback callback, VoiceInteractor.Prompt message, Bundle extras) {
+ CompleteVoiceRequest request = new CompleteVoiceRequest(callingPackage,
+ Binder.getCallingUid(), callback, VoiceInteractionSession.this,
+ message, extras);
+ addRequest(request);
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_START_COMPLETE_VOICE,
+ request));
+ return request.mInterface;
+ }
+
+ @Override
+ public IVoiceInteractorRequest startAbortVoice(String callingPackage,
+ IVoiceInteractorCallback callback, VoiceInteractor.Prompt message, Bundle extras) {
+ AbortVoiceRequest request = new AbortVoiceRequest(callingPackage,
+ Binder.getCallingUid(), callback, VoiceInteractionSession.this,
+ message, extras);
+ addRequest(request);
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_START_ABORT_VOICE,
+ request));
+ return request.mInterface;
+ }
+
+ @Override
+ public IVoiceInteractorRequest startCommand(String callingPackage,
+ IVoiceInteractorCallback callback, String command, Bundle extras) {
+ CommandRequest request = new CommandRequest(callingPackage,
+ Binder.getCallingUid(), callback, VoiceInteractionSession.this,
+ command, extras);
+ addRequest(request);
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_START_COMMAND,
+ request));
+ return request.mInterface;
+ }
+
+ @Override
+ public boolean[] supportsCommands(String callingPackage, String[] commands) {
+ Message msg = mHandlerCaller.obtainMessageIOO(MSG_SUPPORTS_COMMANDS,
+ 0, commands, null);
+ SomeArgs args = mHandlerCaller.sendMessageAndWait(msg);
+ if (args != null) {
+ boolean[] res = (boolean[])args.arg1;
+ args.recycle();
+ return res;
+ }
+ return new boolean[commands.length];
+ }
+
+ @Override
+ public void notifyDirectActionsChanged(int taskId, IBinder assistToken) {
+ mHandlerCaller.getHandler().sendMessage(PooledLambda.obtainMessage(
+ VoiceInteractionSession::onDirectActionsInvalidated,
+ VoiceInteractionSession.this, new ActivityId(taskId, assistToken))
+ );
+ }
+
+ @Override
+ public void setKillCallback(ICancellationSignal callback) {
+ mKillCallback = callback;
+ }
+ };
+
+ final IVoiceInteractionSession mSession = new IVoiceInteractionSession.Stub() {
+ @Override
+ public void show(Bundle sessionArgs, int flags,
+ IVoiceInteractionSessionShowCallback showCallback) {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(MSG_SHOW,
+ flags, sessionArgs, showCallback));
+ }
+
+ @Override
+ public void hide() {
+ // Remove any pending messages to show the session
+ mHandlerCaller.removeMessages(MSG_SHOW);
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_HIDE));
+ }
+
+ @Override
+ public void handleAssist(final int taskId, final IBinder assistToken, final Bundle data,
+ final AssistStructure structure, final AssistContent content, final int index,
+ final int count) {
+ // We want to pre-warm the AssistStructure before handing it off to the main
+ // thread. We also want to do this on a separate thread, so that if the app
+ // is for some reason slow (due to slow filling in of async children in the
+ // structure), we don't block other incoming IPCs (such as the screenshot) to
+ // us (since we are a oneway interface, they get serialized). (Okay?)
+ Thread retriever = new Thread("AssistStructure retriever") {
+ @Override
+ public void run() {
+ Throwable failure = null;
+ if (structure != null) {
+ try {
+ structure.ensureData();
+ } catch (Throwable e) {
+ Log.w(TAG, "Failure retrieving AssistStructure", e);
+ failure = e;
+ }
+ }
+
+ SomeArgs args = SomeArgs.obtain();
+ args.argi1 = taskId;
+ args.arg1 = data;
+ args.arg2 = (failure == null) ? structure : null;
+ args.arg3 = failure;
+ args.arg4 = content;
+ args.arg5 = assistToken;
+ args.argi5 = index;
+ args.argi6 = count;
+
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(
+ MSG_HANDLE_ASSIST, args));
+ }
+ };
+ retriever.start();
+ }
+
+ @Override
+ public void handleScreenshot(Bitmap screenshot) {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_HANDLE_SCREENSHOT,
+ screenshot));
+ }
+
+ @Override
+ public void taskStarted(Intent intent, int taskId) {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIO(MSG_TASK_STARTED,
+ taskId, intent));
+ }
+
+ @Override
+ public void taskFinished(Intent intent, int taskId) {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIO(MSG_TASK_FINISHED,
+ taskId, intent));
+ }
+
+ @Override
+ public void closeSystemDialogs() {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_CLOSE_SYSTEM_DIALOGS));
+ }
+
+ @Override
+ public void onLockscreenShown() {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_ON_LOCKSCREEN_SHOWN));
+ }
+
+ @Override
+ public void destroy() {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_DESTROY));
+ }
+ };
+
+ /**
+ * Base class representing a request from a voice-driver app to perform a particular
+ * voice operation with the user. See related subclasses for the types of requests
+ * that are possible.
+ */
+ public static class Request {
+ final IVoiceInteractorRequest mInterface = new IVoiceInteractorRequest.Stub() {
+ @Override
+ public void cancel() throws RemoteException {
+ VoiceInteractionSession session = mSession.get();
+ if (session != null) {
+ session.mHandlerCaller.sendMessage(
+ session.mHandlerCaller.obtainMessageO(MSG_CANCEL, Request.this));
+ }
+ }
+ };
+ final String mCallingPackage;
+ final int mCallingUid;
+ final IVoiceInteractorCallback mCallback;
+ final WeakReference<VoiceInteractionSession> mSession;
+ final Bundle mExtras;
+
+ Request(String packageName, int uid, IVoiceInteractorCallback callback,
+ VoiceInteractionSession session, Bundle extras) {
+ mCallingPackage = packageName;
+ mCallingUid = uid;
+ mCallback = callback;
+ mSession = session.mWeakRef;
+ mExtras = extras;
+ }
+
+ /**
+ * Return the uid of the application that initiated the request.
+ */
+ public int getCallingUid() {
+ return mCallingUid;
+ }
+
+ /**
+ * Return the package name of the application that initiated the request.
+ */
+ public String getCallingPackage() {
+ return mCallingPackage;
+ }
+
+ /**
+ * Return any additional extra information that was supplied as part of the request.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Check whether this request is currently active. A request becomes inactive after
+ * calling {@link #cancel} or a final result method that completes the request. After
+ * this point, further interactions with the request will result in
+ * {@link java.lang.IllegalStateException} errors; you should not catch these errors,
+ * but can use this method if you need to determine the state of the request. Returns
+ * true if the request is still active.
+ */
+ public boolean isActive() {
+ VoiceInteractionSession session = mSession.get();
+ if (session == null) {
+ return false;
+ }
+ return session.isRequestActive(mInterface.asBinder());
+ }
+
+ void finishRequest() {
+ VoiceInteractionSession session = mSession.get();
+ if (session == null) {
+ throw new IllegalStateException("VoiceInteractionSession has been destroyed");
+ }
+ Request req = session.removeRequest(mInterface.asBinder());
+ if (req == null) {
+ throw new IllegalStateException("Request not active: " + this);
+ } else if (req != this) {
+ throw new IllegalStateException("Current active request " + req
+ + " not same as calling request " + this);
+ }
+ }
+
+ /**
+ * Ask the app to cancel this current request.
+ * This also finishes the request (it is no longer active).
+ */
+ public void cancel() {
+ try {
+ if (DEBUG) Log.d(TAG, "sendCancelResult: req=" + mInterface);
+ finishRequest();
+ mCallback.deliverCancel(mInterface);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ DebugUtils.buildShortClassTag(this, sb);
+ sb.append(" ");
+ sb.append(mInterface.asBinder());
+ sb.append(" pkg=");
+ sb.append(mCallingPackage);
+ sb.append(" uid=");
+ UserHandle.formatUid(sb, mCallingUid);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.print(prefix); writer.print("mInterface=");
+ writer.println(mInterface.asBinder());
+ writer.print(prefix); writer.print("mCallingPackage="); writer.print(mCallingPackage);
+ writer.print(" mCallingUid="); UserHandle.formatUid(writer, mCallingUid);
+ writer.println();
+ writer.print(prefix); writer.print("mCallback=");
+ writer.println(mCallback.asBinder());
+ if (mExtras != null) {
+ writer.print(prefix); writer.print("mExtras=");
+ writer.println(mExtras);
+ }
+ }
+ }
+
+ /**
+ * A request for confirmation from the user of an operation, as per
+ * {@link android.app.VoiceInteractor.ConfirmationRequest
+ * VoiceInteractor.ConfirmationRequest}.
+ */
+ public static final class ConfirmationRequest extends Request {
+ final VoiceInteractor.Prompt mPrompt;
+
+ ConfirmationRequest(String packageName, int uid, IVoiceInteractorCallback callback,
+ VoiceInteractionSession session, VoiceInteractor.Prompt prompt, Bundle extras) {
+ super(packageName, uid, callback, session, extras);
+ mPrompt = prompt;
+ }
+
+ /**
+ * Return the prompt informing the user of what will happen, as per
+ * {@link android.app.VoiceInteractor.ConfirmationRequest
+ * VoiceInteractor.ConfirmationRequest}.
+ */
+ @Nullable
+ public VoiceInteractor.Prompt getVoicePrompt() {
+ return mPrompt;
+ }
+
+ /**
+ * Return the prompt informing the user of what will happen, as per
+ * {@link android.app.VoiceInteractor.ConfirmationRequest
+ * VoiceInteractor.ConfirmationRequest}.
+ * @deprecated Prefer {@link #getVoicePrompt()} which allows multiple voice prompts.
+ */
+ @Deprecated
+ @Nullable
+ public CharSequence getPrompt() {
+ return (mPrompt != null ? mPrompt.getVoicePromptAt(0) : null);
+ }
+
+ /**
+ * Report that the voice interactor has confirmed the operation with the user, resulting
+ * in a call to
+ * {@link android.app.VoiceInteractor.ConfirmationRequest#onConfirmationResult
+ * VoiceInteractor.ConfirmationRequest.onConfirmationResult}.
+ * This finishes the request (it is no longer active).
+ */
+ public void sendConfirmationResult(boolean confirmed, Bundle result) {
+ try {
+ if (DEBUG) Log.d(TAG, "sendConfirmationResult: req=" + mInterface
+ + " confirmed=" + confirmed + " result=" + result);
+ finishRequest();
+ mCallback.deliverConfirmationResult(mInterface, confirmed, result);
+ } catch (RemoteException e) {
+ }
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mPrompt=");
+ writer.println(mPrompt);
+ }
+ }
+
+ /**
+ * A request for the user to pick from a set of option, as per
+ * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}.
+ */
+ public static final class PickOptionRequest extends Request {
+ final VoiceInteractor.Prompt mPrompt;
+ final VoiceInteractor.PickOptionRequest.Option[] mOptions;
+
+ PickOptionRequest(String packageName, int uid, IVoiceInteractorCallback callback,
+ VoiceInteractionSession session, VoiceInteractor.Prompt prompt,
+ VoiceInteractor.PickOptionRequest.Option[] options, Bundle extras) {
+ super(packageName, uid, callback, session, extras);
+ mPrompt = prompt;
+ mOptions = options;
+ }
+
+ /**
+ * Return the prompt informing the user of what they are picking, as per
+ * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}.
+ */
+ @Nullable
+ public VoiceInteractor.Prompt getVoicePrompt() {
+ return mPrompt;
+ }
+
+ /**
+ * Return the prompt informing the user of what they are picking, as per
+ * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}.
+ * @deprecated Prefer {@link #getVoicePrompt()} which allows multiple voice prompts.
+ */
+ @Deprecated
+ @Nullable
+ public CharSequence getPrompt() {
+ return (mPrompt != null ? mPrompt.getVoicePromptAt(0) : null);
+ }
+
+ /**
+ * Return the set of options the user is picking from, as per
+ * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}.
+ */
+ public VoiceInteractor.PickOptionRequest.Option[] getOptions() {
+ return mOptions;
+ }
+
+ void sendPickOptionResult(boolean finished,
+ VoiceInteractor.PickOptionRequest.Option[] selections, Bundle result) {
+ try {
+ if (DEBUG) Log.d(TAG, "sendPickOptionResult: req=" + mInterface
+ + " finished=" + finished + " selections=" + selections
+ + " result=" + result);
+ if (finished) {
+ finishRequest();
+ }
+ mCallback.deliverPickOptionResult(mInterface, finished, selections, result);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Report an intermediate option selection from the request, without completing it (the
+ * request is still active and the app is waiting for the final option selection),
+ * resulting in a call to
+ * {@link android.app.VoiceInteractor.PickOptionRequest#onPickOptionResult
+ * VoiceInteractor.PickOptionRequest.onPickOptionResult} with false for finished.
+ */
+ public void sendIntermediatePickOptionResult(
+ VoiceInteractor.PickOptionRequest.Option[] selections, Bundle result) {
+ sendPickOptionResult(false, selections, result);
+ }
+
+ /**
+ * Report the final option selection for the request, completing the request
+ * and resulting in a call to
+ * {@link android.app.VoiceInteractor.PickOptionRequest#onPickOptionResult
+ * VoiceInteractor.PickOptionRequest.onPickOptionResult} with false for finished.
+ * This finishes the request (it is no longer active).
+ */
+ public void sendPickOptionResult(
+ VoiceInteractor.PickOptionRequest.Option[] selections, Bundle result) {
+ sendPickOptionResult(true, selections, result);
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mPrompt=");
+ writer.println(mPrompt);
+ if (mOptions != null) {
+ writer.print(prefix); writer.println("Options:");
+ for (int i=0; i<mOptions.length; i++) {
+ VoiceInteractor.PickOptionRequest.Option op = mOptions[i];
+ writer.print(prefix); writer.print(" #"); writer.print(i); writer.println(":");
+ writer.print(prefix); writer.print(" mLabel=");
+ writer.println(op.getLabel());
+ writer.print(prefix); writer.print(" mIndex=");
+ writer.println(op.getIndex());
+ if (op.countSynonyms() > 0) {
+ writer.print(prefix); writer.println(" Synonyms:");
+ for (int j=0; j<op.countSynonyms(); j++) {
+ writer.print(prefix); writer.print(" #"); writer.print(j);
+ writer.print(": "); writer.println(op.getSynonymAt(j));
+ }
+ }
+ if (op.getExtras() != null) {
+ writer.print(prefix); writer.print(" mExtras=");
+ writer.println(op.getExtras());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * A request to simply inform the user that the voice operation has completed, as per
+ * {@link android.app.VoiceInteractor.CompleteVoiceRequest
+ * VoiceInteractor.CompleteVoiceRequest}.
+ */
+ public static final class CompleteVoiceRequest extends Request {
+ final VoiceInteractor.Prompt mPrompt;
+
+ CompleteVoiceRequest(String packageName, int uid, IVoiceInteractorCallback callback,
+ VoiceInteractionSession session, VoiceInteractor.Prompt prompt, Bundle extras) {
+ super(packageName, uid, callback, session, extras);
+ mPrompt = prompt;
+ }
+
+ /**
+ * Return the message informing the user of the completion, as per
+ * {@link android.app.VoiceInteractor.CompleteVoiceRequest
+ * VoiceInteractor.CompleteVoiceRequest}.
+ */
+ @Nullable
+ public VoiceInteractor.Prompt getVoicePrompt() {
+ return mPrompt;
+ }
+
+ /**
+ * Return the message informing the user of the completion, as per
+ * {@link android.app.VoiceInteractor.CompleteVoiceRequest
+ * VoiceInteractor.CompleteVoiceRequest}.
+ * @deprecated Prefer {@link #getVoicePrompt()} which allows a separate visual message.
+ */
+ @Deprecated
+ @Nullable
+ public CharSequence getMessage() {
+ return (mPrompt != null ? mPrompt.getVoicePromptAt(0) : null);
+ }
+
+ /**
+ * Report that the voice interactor has finished completing the voice operation, resulting
+ * in a call to
+ * {@link android.app.VoiceInteractor.CompleteVoiceRequest#onCompleteResult
+ * VoiceInteractor.CompleteVoiceRequest.onCompleteResult}.
+ * This finishes the request (it is no longer active).
+ */
+ public void sendCompleteResult(Bundle result) {
+ try {
+ if (DEBUG) Log.d(TAG, "sendCompleteVoiceResult: req=" + mInterface
+ + " result=" + result);
+ finishRequest();
+ mCallback.deliverCompleteVoiceResult(mInterface, result);
+ } catch (RemoteException e) {
+ }
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mPrompt=");
+ writer.println(mPrompt);
+ }
+ }
+
+ /**
+ * A request to report that the current user interaction can not be completed with voice, as per
+ * {@link android.app.VoiceInteractor.AbortVoiceRequest VoiceInteractor.AbortVoiceRequest}.
+ */
+ public static final class AbortVoiceRequest extends Request {
+ final VoiceInteractor.Prompt mPrompt;
+
+ AbortVoiceRequest(String packageName, int uid, IVoiceInteractorCallback callback,
+ VoiceInteractionSession session, VoiceInteractor.Prompt prompt, Bundle extras) {
+ super(packageName, uid, callback, session, extras);
+ mPrompt = prompt;
+ }
+
+ /**
+ * Return the message informing the user of the problem, as per
+ * {@link android.app.VoiceInteractor.AbortVoiceRequest VoiceInteractor.AbortVoiceRequest}.
+ */
+ @Nullable
+ public VoiceInteractor.Prompt getVoicePrompt() {
+ return mPrompt;
+ }
+
+ /**
+ * Return the message informing the user of the problem, as per
+ * {@link android.app.VoiceInteractor.AbortVoiceRequest VoiceInteractor.AbortVoiceRequest}.
+ * @deprecated Prefer {@link #getVoicePrompt()} which allows a separate visual message.
+ */
+ @Deprecated
+ @Nullable
+ public CharSequence getMessage() {
+ return (mPrompt != null ? mPrompt.getVoicePromptAt(0) : null);
+ }
+
+ /**
+ * Report that the voice interactor has finished aborting the voice operation, resulting
+ * in a call to
+ * {@link android.app.VoiceInteractor.AbortVoiceRequest#onAbortResult
+ * VoiceInteractor.AbortVoiceRequest.onAbortResult}. This finishes the request (it
+ * is no longer active).
+ */
+ public void sendAbortResult(Bundle result) {
+ try {
+ if (DEBUG) Log.d(TAG, "sendConfirmResult: req=" + mInterface
+ + " result=" + result);
+ finishRequest();
+ mCallback.deliverAbortVoiceResult(mInterface, result);
+ } catch (RemoteException e) {
+ }
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mPrompt=");
+ writer.println(mPrompt);
+ }
+ }
+
+ /**
+ * A generic vendor-specific request, as per
+ * {@link android.app.VoiceInteractor.CommandRequest VoiceInteractor.CommandRequest}.
+ */
+ public static final class CommandRequest extends Request {
+ final String mCommand;
+
+ CommandRequest(String packageName, int uid, IVoiceInteractorCallback callback,
+ VoiceInteractionSession session, String command, Bundle extras) {
+ super(packageName, uid, callback, session, extras);
+ mCommand = command;
+ }
+
+ /**
+ * Return the command that is being executed, as per
+ * {@link android.app.VoiceInteractor.CommandRequest VoiceInteractor.CommandRequest}.
+ */
+ public String getCommand() {
+ return mCommand;
+ }
+
+ void sendCommandResult(boolean finished, Bundle result) {
+ try {
+ if (DEBUG) Log.d(TAG, "sendCommandResult: req=" + mInterface
+ + " result=" + result);
+ if (finished) {
+ finishRequest();
+ }
+ mCallback.deliverCommandResult(mInterface, finished, result);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Report an intermediate result of the request, without completing it (the request
+ * is still active and the app is waiting for the final result), resulting in a call to
+ * {@link android.app.VoiceInteractor.CommandRequest#onCommandResult
+ * VoiceInteractor.CommandRequest.onCommandResult} with false for isCompleted.
+ */
+ public void sendIntermediateResult(Bundle result) {
+ sendCommandResult(false, result);
+ }
+
+ /**
+ * Report the final result of the request, completing the request and resulting in a call to
+ * {@link android.app.VoiceInteractor.CommandRequest#onCommandResult
+ * VoiceInteractor.CommandRequest.onCommandResult} with true for isCompleted.
+ * This finishes the request (it is no longer active).
+ */
+ public void sendResult(Bundle result) {
+ sendCommandResult(true, result);
+ }
+
+ void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ writer.print(prefix); writer.print("mCommand=");
+ writer.println(mCommand);
+ }
+ }
+
+ static final int MSG_START_CONFIRMATION = 1;
+ static final int MSG_START_PICK_OPTION = 2;
+ static final int MSG_START_COMPLETE_VOICE = 3;
+ static final int MSG_START_ABORT_VOICE = 4;
+ static final int MSG_START_COMMAND = 5;
+ static final int MSG_SUPPORTS_COMMANDS = 6;
+ static final int MSG_CANCEL = 7;
+
+ static final int MSG_TASK_STARTED = 100;
+ static final int MSG_TASK_FINISHED = 101;
+ static final int MSG_CLOSE_SYSTEM_DIALOGS = 102;
+ static final int MSG_DESTROY = 103;
+ static final int MSG_HANDLE_ASSIST = 104;
+ static final int MSG_HANDLE_SCREENSHOT = 105;
+ static final int MSG_SHOW = 106;
+ static final int MSG_HIDE = 107;
+ static final int MSG_ON_LOCKSCREEN_SHOWN = 108;
+
+ class MyCallbacks implements HandlerCaller.Callback, SoftInputWindow.Callback {
+ @Override
+ public void executeMessage(Message msg) {
+ SomeArgs args = null;
+ switch (msg.what) {
+ case MSG_START_CONFIRMATION:
+ if (DEBUG) Log.d(TAG, "onConfirm: req=" + msg.obj);
+ onRequestConfirmation((ConfirmationRequest) msg.obj);
+ break;
+ case MSG_START_PICK_OPTION:
+ if (DEBUG) Log.d(TAG, "onPickOption: req=" + msg.obj);
+ onRequestPickOption((PickOptionRequest) msg.obj);
+ break;
+ case MSG_START_COMPLETE_VOICE:
+ if (DEBUG) Log.d(TAG, "onCompleteVoice: req=" + msg.obj);
+ onRequestCompleteVoice((CompleteVoiceRequest) msg.obj);
+ break;
+ case MSG_START_ABORT_VOICE:
+ if (DEBUG) Log.d(TAG, "onAbortVoice: req=" + msg.obj);
+ onRequestAbortVoice((AbortVoiceRequest) msg.obj);
+ break;
+ case MSG_START_COMMAND:
+ if (DEBUG) Log.d(TAG, "onCommand: req=" + msg.obj);
+ onRequestCommand((CommandRequest) msg.obj);
+ break;
+ case MSG_SUPPORTS_COMMANDS:
+ args = (SomeArgs)msg.obj;
+ if (DEBUG) Log.d(TAG, "onGetSupportedCommands: cmds=" + args.arg1);
+ args.arg1 = onGetSupportedCommands((String[]) args.arg1);
+ args.complete();
+ args = null;
+ break;
+ case MSG_CANCEL:
+ if (DEBUG) Log.d(TAG, "onCancel: req=" + ((Request)msg.obj));
+ onCancelRequest((Request) msg.obj);
+ break;
+ case MSG_TASK_STARTED:
+ if (DEBUG) Log.d(TAG, "onTaskStarted: intent=" + msg.obj
+ + " taskId=" + msg.arg1);
+ onTaskStarted((Intent) msg.obj, msg.arg1);
+ break;
+ case MSG_TASK_FINISHED:
+ if (DEBUG) Log.d(TAG, "onTaskFinished: intent=" + msg.obj
+ + " taskId=" + msg.arg1);
+ onTaskFinished((Intent) msg.obj, msg.arg1);
+ break;
+ case MSG_CLOSE_SYSTEM_DIALOGS:
+ if (DEBUG) Log.d(TAG, "onCloseSystemDialogs");
+ onCloseSystemDialogs();
+ break;
+ case MSG_DESTROY:
+ if (DEBUG) Log.d(TAG, "doDestroy");
+ doDestroy();
+ break;
+ case MSG_HANDLE_ASSIST:
+ args = (SomeArgs)msg.obj;
+ if (DEBUG) Log.d(TAG, "onHandleAssist: taskId=" + args.argi1
+ + "assistToken=" + args.arg5 + " data=" + args.arg1
+ + " structure=" + args.arg2 + " content=" + args.arg3
+ + " activityIndex=" + args.argi5 + " activityCount=" + args.argi6);
+ doOnHandleAssist(args.argi1, (IBinder) args.arg5, (Bundle) args.arg1,
+ (AssistStructure) args.arg2, (Throwable) args.arg3,
+ (AssistContent) args.arg4, args.argi5, args.argi6);
+ break;
+ case MSG_HANDLE_SCREENSHOT:
+ if (DEBUG) Log.d(TAG, "onHandleScreenshot: " + msg.obj);
+ onHandleScreenshot((Bitmap) msg.obj);
+ break;
+ case MSG_SHOW:
+ args = (SomeArgs)msg.obj;
+ if (DEBUG) Log.d(TAG, "doShow: args=" + args.arg1
+ + " flags=" + msg.arg1
+ + " showCallback=" + args.arg2);
+ doShow((Bundle) args.arg1, msg.arg1,
+ (IVoiceInteractionSessionShowCallback) args.arg2);
+ break;
+ case MSG_HIDE:
+ if (DEBUG) Log.d(TAG, "doHide");
+ doHide();
+ break;
+ case MSG_ON_LOCKSCREEN_SHOWN:
+ if (DEBUG) Log.d(TAG, "onLockscreenShown");
+ onLockscreenShown();
+ break;
+ }
+ if (args != null) {
+ args.recycle();
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ VoiceInteractionSession.this.onBackPressed();
+ }
+ }
+
+ final MyCallbacks mCallbacks = new MyCallbacks();
+
+ /**
+ * Information about where interesting parts of the input method UI appear.
+ */
+ public static final class Insets {
+ /**
+ * This is the part of the UI that is the main content. It is
+ * used to determine the basic space needed, to resize/pan the
+ * application behind. It is assumed that this inset does not
+ * change very much, since any change will cause a full resize/pan
+ * of the application behind. This value is relative to the top edge
+ * of the input method window.
+ */
+ public final Rect contentInsets = new Rect();
+
+ /**
+ * This is the region of the UI that is touchable. It is used when
+ * {@link #touchableInsets} is set to {@link #TOUCHABLE_INSETS_REGION}.
+ * The region should be specified relative to the origin of the window frame.
+ */
+ public final Region touchableRegion = new Region();
+
+ /**
+ * Option for {@link #touchableInsets}: the entire window frame
+ * can be touched.
+ */
+ public static final int TOUCHABLE_INSETS_FRAME
+ = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME;
+
+ /**
+ * Option for {@link #touchableInsets}: the area inside of
+ * the content insets can be touched.
+ */
+ public static final int TOUCHABLE_INSETS_CONTENT
+ = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT;
+
+ /**
+ * Option for {@link #touchableInsets}: the region specified by
+ * {@link #touchableRegion} can be touched.
+ */
+ public static final int TOUCHABLE_INSETS_REGION
+ = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
+
+ /**
+ * Determine which area of the window is touchable by the user. May
+ * be one of: {@link #TOUCHABLE_INSETS_FRAME},
+ * {@link #TOUCHABLE_INSETS_CONTENT}, or {@link #TOUCHABLE_INSETS_REGION}.
+ */
+ public int touchableInsets;
+ }
+
+ final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
+ new ViewTreeObserver.OnComputeInternalInsetsListener() {
+ public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
+ onComputeInsets(mTmpInsets);
+ info.contentInsets.set(mTmpInsets.contentInsets);
+ info.visibleInsets.set(mTmpInsets.contentInsets);
+ info.touchableRegion.set(mTmpInsets.touchableRegion);
+ info.setTouchableInsets(mTmpInsets.touchableInsets);
+ }
+ };
+
+ public VoiceInteractionSession(Context context) {
+ this(context, new Handler());
+ }
+
+ public VoiceInteractionSession(Context context, Handler handler) {
+ mContext = context;
+ mHandlerCaller = new HandlerCaller(context, handler.getLooper(),
+ mCallbacks, true);
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ void addRequest(Request req) {
+ synchronized (this) {
+ mActiveRequests.put(req.mInterface.asBinder(), req);
+ }
+ }
+
+ boolean isRequestActive(IBinder reqInterface) {
+ synchronized (this) {
+ return mActiveRequests.containsKey(reqInterface);
+ }
+ }
+
+ Request removeRequest(IBinder reqInterface) {
+ synchronized (this) {
+ return mActiveRequests.remove(reqInterface);
+ }
+ }
+
+ void doCreate(IVoiceInteractionManagerService service, IBinder token) {
+ mSystemService = service;
+ mToken = token;
+ onCreate();
+ }
+
+ void doShow(Bundle args, int flags, final IVoiceInteractionSessionShowCallback showCallback) {
+ if (DEBUG) Log.v(TAG, "Showing window: mWindowAdded=" + mWindowAdded
+ + " mWindowVisible=" + mWindowVisible);
+
+ if (mInShowWindow) {
+ Log.w(TAG, "Re-entrance in to showWindow");
+ return;
+ }
+
+ try {
+ mInShowWindow = true;
+ onPrepareShow(args, flags);
+ if (!mWindowVisible) {
+ ensureWindowAdded();
+ }
+ onShow(args, flags);
+ if (!mWindowVisible) {
+ mWindowVisible = true;
+ if (mUiEnabled) {
+ mWindow.show();
+ }
+ }
+ if (showCallback != null) {
+ if (mUiEnabled) {
+ mRootView.invalidate();
+ mRootView.getViewTreeObserver().addOnPreDrawListener(
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ mRootView.getViewTreeObserver().removeOnPreDrawListener(this);
+ try {
+ showCallback.onShown();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error calling onShown", e);
+ }
+ return true;
+ }
+ });
+ } else {
+ try {
+ showCallback.onShown();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error calling onShown", e);
+ }
+ }
+ }
+ } finally {
+ mWindowWasVisible = true;
+ mInShowWindow = false;
+ }
+ }
+
+ void doHide() {
+ if (mWindowVisible) {
+ ensureWindowHidden();
+ mWindowVisible = false;
+ onHide();
+ }
+ }
+
+ void doDestroy() {
+ onDestroy();
+ if (mKillCallback != null) {
+ try {
+ mKillCallback.cancel();
+ } catch (RemoteException e) {
+ /* ignore */
+ }
+ mKillCallback = null;
+ }
+ if (mInitialized) {
+ mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener(
+ mInsetsComputer);
+ if (mWindowAdded) {
+ mWindow.dismiss();
+ mWindowAdded = false;
+ }
+ mInitialized = false;
+ }
+ }
+
+ void ensureWindowCreated() {
+ if (mInitialized) {
+ return;
+ }
+
+ if (!mUiEnabled) {
+ throw new IllegalStateException("setUiEnabled is false");
+ }
+
+ mInitialized = true;
+ mInflater = (LayoutInflater)mContext.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ mWindow = new SoftInputWindow(mContext, "VoiceInteractionSession", mTheme,
+ mCallbacks, this, mDispatcherState,
+ WindowManager.LayoutParams.TYPE_VOICE_INTERACTION, Gravity.BOTTOM, true);
+ mWindow.getWindow().getAttributes().setFitInsetsTypes(0 /* types */);
+ mWindow.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED |
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
+ WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
+
+ mThemeAttrs = mContext.obtainStyledAttributes(android.R.styleable.VoiceInteractionSession);
+ mRootView = mInflater.inflate(
+ com.android.internal.R.layout.voice_interaction_session, null);
+ mRootView.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ mWindow.setContentView(mRootView);
+ mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer);
+
+ mContentFrame = (FrameLayout)mRootView.findViewById(android.R.id.content);
+
+ mWindow.getWindow().setLayout(MATCH_PARENT, MATCH_PARENT);
+ mWindow.setToken(mToken);
+ }
+
+ void ensureWindowAdded() {
+ if (mUiEnabled && !mWindowAdded) {
+ mWindowAdded = true;
+ ensureWindowCreated();
+ View v = onCreateContentView();
+ if (v != null) {
+ setContentView(v);
+ }
+ }
+ }
+
+ void ensureWindowHidden() {
+ if (mWindow != null) {
+ mWindow.hide();
+ }
+ }
+
+ /**
+ * Equivalent to {@link VoiceInteractionService#setDisabledShowContext
+ * VoiceInteractionService.setDisabledShowContext(int)}.
+ */
+ public void setDisabledShowContext(int flags) {
+ try {
+ mSystemService.setDisabledShowContext(flags);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Equivalent to {@link VoiceInteractionService#getDisabledShowContext
+ * VoiceInteractionService.getDisabledShowContext}.
+ */
+ public int getDisabledShowContext() {
+ try {
+ return mSystemService.getDisabledShowContext();
+ } catch (RemoteException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Return which show context flags have been disabled by the user through the system
+ * settings UI, so the session will never get this data. Returned flags are any combination of
+ * {@link VoiceInteractionSession#SHOW_WITH_ASSIST VoiceInteractionSession.SHOW_WITH_ASSIST} and
+ * {@link VoiceInteractionSession#SHOW_WITH_SCREENSHOT
+ * VoiceInteractionSession.SHOW_WITH_SCREENSHOT}. Note that this only tells you about
+ * global user settings, not about restrictions that may be applied contextual based on
+ * the current application the user is in or other transient states.
+ */
+ public int getUserDisabledShowContext() {
+ try {
+ return mSystemService.getUserDisabledShowContext();
+ } catch (RemoteException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Show the UI for this session. This asks the system to go through the process of showing
+ * your UI, which will eventually culminate in {@link #onShow}. This is similar to calling
+ * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}.
+ * @param args Arbitrary arguments that will be propagated {@link #onShow}.
+ * @param flags Indicates additional optional behavior that should be performed. May
+ * be any combination of
+ * {@link VoiceInteractionSession#SHOW_WITH_ASSIST VoiceInteractionSession.SHOW_WITH_ASSIST} and
+ * {@link VoiceInteractionSession#SHOW_WITH_SCREENSHOT
+ * VoiceInteractionSession.SHOW_WITH_SCREENSHOT}
+ * to request that the system generate and deliver assist data on the current foreground
+ * app as part of showing the session UI.
+ */
+ public void show(Bundle args, int flags) {
+ if (mToken == null) {
+ throw new IllegalStateException("Can't call before onCreate()");
+ }
+ try {
+ mSystemService.showSessionFromSession(mToken, args, flags);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Hide the session's UI, if currently shown. Call this when you are done with your
+ * user interaction.
+ */
+ public void hide() {
+ if (mToken == null) {
+ throw new IllegalStateException("Can't call before onCreate()");
+ }
+ try {
+ mSystemService.hideSessionFromSession(mToken);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Control whether the UI layer for this session is enabled. It is enabled by default.
+ * If set to false, you will not be able to provide a UI through {@link #onCreateContentView()}.
+ */
+ public void setUiEnabled(boolean enabled) {
+ if (mUiEnabled != enabled) {
+ mUiEnabled = enabled;
+ if (mWindowVisible) {
+ if (enabled) {
+ ensureWindowAdded();
+ mWindow.show();
+ } else {
+ ensureWindowHidden();
+ }
+ }
+ }
+ }
+
+ /**
+ * You can call this to customize the theme used by your IME's window.
+ * This must be set before {@link #onCreate}, so you
+ * will typically call it in your constructor with the resource ID
+ * of your custom theme.
+ */
+ public void setTheme(int theme) {
+ if (mWindow != null) {
+ throw new IllegalStateException("Must be called before onCreate()");
+ }
+ mTheme = theme;
+ }
+
+ /**
+ * Ask that a new activity be started for voice interaction. This will create a
+ * new dedicated task in the activity manager for this voice interaction session;
+ * this means that {@link Intent#FLAG_ACTIVITY_NEW_TASK Intent.FLAG_ACTIVITY_NEW_TASK}
+ * will be set for you to make it a new task.
+ *
+ * <p>The newly started activity will be displayed to the user in a special way, as
+ * a layer under the voice interaction UI.</p>
+ *
+ * <p>As the voice activity runs, it can retrieve a {@link android.app.VoiceInteractor}
+ * through which it can perform voice interactions through your session. These requests
+ * for voice interactions will appear as callbacks on {@link #onGetSupportedCommands},
+ * {@link #onRequestConfirmation}, {@link #onRequestPickOption},
+ * {@link #onRequestCompleteVoice}, {@link #onRequestAbortVoice},
+ * or {@link #onRequestCommand}
+ *
+ * <p>You will receive a call to {@link #onTaskStarted} when the task starts up
+ * and {@link #onTaskFinished} when the last activity has finished.
+ *
+ * @param intent The Intent to start this voice interaction. The given Intent will
+ * always have {@link Intent#CATEGORY_VOICE Intent.CATEGORY_VOICE} added to it, since
+ * this is part of a voice interaction.
+ */
+ public void startVoiceActivity(Intent intent) {
+ if (mToken == null) {
+ throw new IllegalStateException("Can't call before onCreate()");
+ }
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(mContext);
+ int res = mSystemService.startVoiceActivity(mToken, intent,
+ intent.resolveType(mContext.getContentResolver()),
+ mContext.getAttributionTag());
+ Instrumentation.checkStartActivityResult(res, intent);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * <p>Ask that a new assistant activity be started. This will create a new task in the
+ * in activity manager: this means that
+ * {@link Intent#FLAG_ACTIVITY_NEW_TASK Intent.FLAG_ACTIVITY_NEW_TASK}
+ * will be set for you to make it a new task.</p>
+ *
+ * <p>The newly started activity will be displayed on top of other activities in the system
+ * in a new layer that is not affected by multi-window mode. Tasks started from this activity
+ * will go into the normal activity layer and not this new layer.</p>
+ *
+ * <p>By default, the system will create a window for the UI for this session. If you are using
+ * an assistant activity instead, then you can disable the window creation by calling
+ * {@link #setUiEnabled} in {@link #onPrepareShow(Bundle, int)}.</p>
+ */
+ public void startAssistantActivity(Intent intent) {
+ if (mToken == null) {
+ throw new IllegalStateException("Can't call before onCreate()");
+ }
+ try {
+ intent.migrateExtraStreamToClipData();
+ intent.prepareToLeaveProcess(mContext);
+ int res = mSystemService.startAssistantActivity(mToken, intent,
+ intent.resolveType(mContext.getContentResolver()),
+ mContext.getAttributionTag());
+ Instrumentation.checkStartActivityResult(res, intent);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Requests a list of supported actions from an app.
+ *
+ * @param activityId Ths activity id of the app to get the actions from.
+ * @param resultExecutor The handler to receive the callback
+ * @param cancellationSignal A signal to cancel the operation in progress,
+ * or {@code null} if none.
+ * @param callback The callback to receive the response
+ */
+ public final void requestDirectActions(@NonNull ActivityId activityId,
+ @Nullable CancellationSignal cancellationSignal,
+ @NonNull @CallbackExecutor Executor resultExecutor,
+ @NonNull Consumer<List<DirectAction>> callback) {
+ Preconditions.checkNotNull(activityId);
+ Preconditions.checkNotNull(resultExecutor);
+ Preconditions.checkNotNull(callback);
+ if (mToken == null) {
+ throw new IllegalStateException("Can't call before onCreate()");
+ }
+
+ if (cancellationSignal != null) {
+ cancellationSignal.throwIfCanceled();
+ }
+
+ final RemoteCallback cancellationCallback = (cancellationSignal != null)
+ ? new RemoteCallback(b -> {
+ if (b != null) {
+ final IBinder cancellation = b.getBinder(
+ VoiceInteractor.KEY_CANCELLATION_SIGNAL);
+ if (cancellation != null) {
+ cancellationSignal.setRemote(ICancellationSignal.Stub.asInterface(
+ cancellation));
+ }
+ }
+ })
+ : null;
+
+ try {
+ mSystemService.requestDirectActions(mToken, activityId.getTaskId(),
+ activityId.getAssistToken(), cancellationCallback,
+ new RemoteCallback(createSafeResultListener((result) -> {
+ List<DirectAction> list;
+ if (result == null) {
+ list = Collections.emptyList();
+ } else {
+ final ParceledListSlice<DirectAction> pls = result.getParcelable(
+ DirectAction.KEY_ACTIONS_LIST);
+ if (pls != null) {
+ final List<DirectAction> receivedList = pls.getList();
+ list = (receivedList != null) ? receivedList : Collections.emptyList();
+ } else {
+ list = Collections.emptyList();
+ }
+ }
+ resultExecutor.execute(() -> callback.accept(list));
+ })));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called when the direct actions are invalidated.
+ */
+ public void onDirectActionsInvalidated(@NonNull ActivityId activityId) {
+
+ }
+
+ /**
+ * Asks that an action be performed by the app. This will send a request to the app which
+ * provided this action.
+ *
+ * <p> An action could take time to execute and the result is provided asynchronously
+ * via a callback. If the action is taking longer and you want to cancel its execution
+ * you can pass in a cancellation signal through which to notify the app to abort the
+ * action.
+ *
+ * @param action The action to be performed.
+ * @param extras Any optional extras sent to the app as part of the request
+ * @param cancellationSignal A signal to cancel the operation in progress,
+ * or {@code null} if none.
+ * @param resultExecutor The handler to receive the callback.
+ * @param resultListener The callback to receive the response.
+ *
+ * @see #requestDirectActions(ActivityId, CancellationSignal, Executor, Consumer)
+ * @see Activity#onGetDirectActions(CancellationSignal, Consumer)
+ */
+ public final void performDirectAction(@NonNull DirectAction action, @Nullable Bundle extras,
+ @Nullable CancellationSignal cancellationSignal,
+ @NonNull @CallbackExecutor Executor resultExecutor,
+ @NonNull Consumer<Bundle> resultListener) {
+ if (mToken == null) {
+ throw new IllegalStateException("Can't call before onCreate()");
+ }
+ Preconditions.checkNotNull(resultExecutor);
+ Preconditions.checkNotNull(resultListener);
+
+ if (cancellationSignal != null) {
+ cancellationSignal.throwIfCanceled();
+ }
+
+ final RemoteCallback cancellationCallback = (cancellationSignal != null)
+ ? new RemoteCallback(createSafeResultListener(b -> {
+ if (b != null) {
+ final IBinder cancellation = b.getBinder(
+ VoiceInteractor.KEY_CANCELLATION_SIGNAL);
+ if (cancellation != null) {
+ cancellationSignal.setRemote(ICancellationSignal.Stub.asInterface(
+ cancellation));
+ }
+ }
+ }))
+ : null;
+
+ final RemoteCallback resultCallback = new RemoteCallback(createSafeResultListener(b -> {
+ if (b != null) {
+ resultExecutor.execute(() -> resultListener.accept(b));
+ } else {
+ resultExecutor.execute(() -> resultListener.accept(Bundle.EMPTY));
+ }
+ }));
+
+ try {
+ mSystemService.performDirectAction(mToken, action.getId(), extras,
+ action.getTaskId(), action.getActivityId(), cancellationCallback,
+ resultCallback);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set whether this session will keep the device awake while it is running a voice
+ * activity. By default, the system holds a wake lock for it while in this state,
+ * so that it can work even if the screen is off. Setting this to false removes that
+ * wake lock, allowing the CPU to go to sleep. This is typically used if the
+ * session decides it has been waiting too long for a response from the user and
+ * doesn't want to let this continue to drain the battery.
+ *
+ * <p>Passing false here will release the wake lock, and you can call later with
+ * true to re-acquire it. It will also be automatically re-acquired for you each
+ * time you start a new voice activity task -- that is when you call
+ * {@link #startVoiceActivity}.</p>
+ */
+ public void setKeepAwake(boolean keepAwake) {
+ if (mToken == null) {
+ throw new IllegalStateException("Can't call before onCreate()");
+ }
+ try {
+ mSystemService.setKeepAwake(mToken, keepAwake);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Request that all system dialogs (and status bar shade etc) be closed, allowing
+ * access to the session's UI. This will <em>not</em> cause the lock screen to be
+ * dismissed.
+ */
+ public void closeSystemDialogs() {
+ if (mToken == null) {
+ throw new IllegalStateException("Can't call before onCreate()");
+ }
+ try {
+ mSystemService.closeSystemDialogs(mToken);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Convenience for inflating views.
+ */
+ public LayoutInflater getLayoutInflater() {
+ ensureWindowCreated();
+ return mInflater;
+ }
+
+ /**
+ * Retrieve the window being used to show the session's UI.
+ */
+ public Dialog getWindow() {
+ ensureWindowCreated();
+ return mWindow;
+ }
+
+ /**
+ * Finish the session. This completely destroys the session -- the next time it is shown,
+ * an entirely new one will be created. You do not normally call this function; instead,
+ * use {@link #hide} and allow the system to destroy your session if it needs its RAM.
+ */
+ public void finish() {
+ if (mToken == null) {
+ throw new IllegalStateException("Can't call before onCreate()");
+ }
+ try {
+ mSystemService.finish(mToken);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Initiatize a new session. At this point you don't know exactly what this
+ * session will be used for; you will find that out in {@link #onShow}.
+ */
+ public void onCreate() {
+ doOnCreate();
+ }
+
+ private void doOnCreate() {
+ mTheme = mTheme != 0 ? mTheme
+ : com.android.internal.R.style.Theme_DeviceDefault_VoiceInteractionSession;
+ }
+
+ /**
+ * Called prior to {@link #onShow} before any UI setup has occurred. Not generally useful.
+ *
+ * @param args The arguments that were supplied to
+ * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}.
+ * @param showFlags The show flags originally provided to
+ * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}.
+ */
+ public void onPrepareShow(Bundle args, int showFlags) {
+ }
+
+ /**
+ * Called when the session UI is going to be shown. This is called after
+ * {@link #onCreateContentView} (if the session's content UI needed to be created) and
+ * immediately prior to the window being shown. This may be called while the window
+ * is already shown, if a show request has come in while it is shown, to allow you to
+ * update the UI to match the new show arguments.
+ *
+ * @param args The arguments that were supplied to
+ * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}.
+ * @param showFlags The show flags originally provided to
+ * {@link VoiceInteractionService#showSession VoiceInteractionService.showSession}.
+ */
+ public void onShow(Bundle args, int showFlags) {
+ }
+
+ /**
+ * Called immediately after stopping to show the session UI.
+ */
+ public void onHide() {
+ }
+
+ /**
+ * Last callback to the session as it is being finished.
+ */
+ public void onDestroy() {
+ }
+
+ /**
+ * Hook in which to create the session's UI.
+ */
+ public View onCreateContentView() {
+ return null;
+ }
+
+ public void setContentView(View view) {
+ ensureWindowCreated();
+ mContentFrame.removeAllViews();
+ mContentFrame.addView(view, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ mContentFrame.requestApplyInsets();
+ }
+
+ void doOnHandleAssist(int taskId, IBinder assistToken, Bundle data, AssistStructure structure,
+ Throwable failure, AssistContent content, int index, int count) {
+ if (failure != null) {
+ onAssistStructureFailure(failure);
+ }
+ AssistState assistState = new AssistState(new ActivityId(taskId, assistToken),
+ data, structure, content, index, count);
+ onHandleAssist(assistState);
+ }
+
+ /**
+ * Called when there has been a failure transferring the {@link AssistStructure} to
+ * the assistant. This may happen, for example, if the data is too large and results
+ * in an out of memory exception, or the client has provided corrupt data. This will
+ * be called immediately before {@link #onHandleAssist} and the AssistStructure supplied
+ * there afterwards will be null.
+ *
+ * @param failure The failure exception that was thrown when building the
+ * {@link AssistStructure}.
+ */
+ public void onAssistStructureFailure(Throwable failure) {
+ }
+
+ /**
+ * Called to receive data from the application that the user was currently viewing when
+ * an assist session is started. If the original show request did not specify
+ * {@link #SHOW_WITH_ASSIST}, this method will not be called.
+ *
+ * @param data Arbitrary data supplied by the app through
+ * {@link android.app.Activity#onProvideAssistData Activity.onProvideAssistData}.
+ * May be null if assist data has been disabled by the user or device policy.
+ * @param structure If available, the structure definition of all windows currently
+ * displayed by the app. May be null if assist data has been disabled by the user
+ * or device policy; will be an empty stub if the application has disabled assist
+ * by marking its window as secure.
+ * @param content Additional content data supplied by the app through
+ * {@link android.app.Activity#onProvideAssistContent Activity.onProvideAssistContent}.
+ * May be null if assist data has been disabled by the user or device policy; will
+ * not be automatically filled in with data from the app if the app has marked its
+ * window as secure.
+ *
+ * @deprecated use {@link #onHandleAssist(AssistState)}
+ */
+ @Deprecated
+ public void onHandleAssist(@Nullable Bundle data, @Nullable AssistStructure structure,
+ @Nullable AssistContent content) {
+ }
+
+ /**
+ * Called to receive data from the application that the user was currently viewing when
+ * an assist session is started. If the original show request did not specify
+ * {@link #SHOW_WITH_ASSIST}, this method will not be called.
+ *
+ * <p>This method is called for all activities along with an index and count that indicates
+ * which activity the data is for. {@code index} will be between 0 and {@code count}-1 and
+ * this method is called once for each activity in no particular order. The {@code count}
+ * indicates how many activities to expect assist data for, including the top focused one.
+ * The focused activity can be determined by calling {@link AssistState#isFocused()}.
+ *
+ * <p>To be responsive to assist requests, process assist data as soon as it is received,
+ * without waiting for all queued activities to return assist data.
+ *
+ * @param state The state object capturing the state of an activity.
+ */
+ public void onHandleAssist(@NonNull AssistState state) {
+ if (state.getIndex() == 0) {
+ onHandleAssist(state.getAssistData(), state.getAssistStructure(),
+ state.getAssistContent());
+ } else {
+ onHandleAssistSecondary(state.getAssistData(), state.getAssistStructure(),
+ state.getAssistContent(), state.getIndex(), state.getCount());
+ }
+ }
+
+ /**
+ * Called to receive data from other applications that the user was or is interacting with,
+ * that are currently on the screen in a multi-window display environment, not including the
+ * currently focused activity. This could be
+ * a free-form window, a picture-in-picture window, or another window in a split-screen display.
+ * <p>
+ * This method is very similar to
+ * {@link #onHandleAssist} except that it is called
+ * for additional non-focused activities along with an index and count that indicates
+ * which additional activity the data is for. {@code index} will be between 1 and
+ * {@code count}-1 and this method is called once for each additional window, in no particular
+ * order. The {@code count} indicates how many windows to expect assist data for, including the
+ * top focused activity, which continues to be returned via {@link #onHandleAssist}.
+ * <p>
+ * To be responsive to assist requests, process assist data as soon as it is received,
+ * without waiting for all queued activities to return assist data.
+ *
+ * @param data Arbitrary data supplied by the app through
+ * {@link android.app.Activity#onProvideAssistData Activity.onProvideAssistData}.
+ * May be null if assist data has been disabled by the user or device policy.
+ * @param structure If available, the structure definition of all windows currently
+ * displayed by the app. May be null if assist data has been disabled by the user
+ * or device policy; will be an empty stub if the application has disabled assist
+ * by marking its window as secure.
+ * @param content Additional content data supplied by the app through
+ * {@link android.app.Activity#onProvideAssistContent Activity.onProvideAssistContent}.
+ * May be null if assist data has been disabled by the user or device policy; will
+ * not be automatically filled in with data from the app if the app has marked its
+ * window as secure.
+ * @param index the index of the additional activity that this data
+ * is for.
+ * @param count the total number of additional activities for which the assist data is being
+ * returned, including the focused activity that is returned via
+ * {@link #onHandleAssist}.
+ *
+ * @deprecated use {@link #onHandleAssist(AssistState)}
+ */
+ @Deprecated
+ public void onHandleAssistSecondary(@Nullable Bundle data, @Nullable AssistStructure structure,
+ @Nullable AssistContent content, int index, int count) {
+ }
+
+ /**
+ * Called to receive a screenshot of what the user was currently viewing when an assist
+ * session is started. May be null if screenshots are disabled by the user, policy,
+ * or application. If the original show request did not specify
+ * {@link #SHOW_WITH_SCREENSHOT}, this method will not be called.
+ */
+ public void onHandleScreenshot(@Nullable Bitmap screenshot) {
+ }
+
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Called when the user presses the back button while focus is in the session UI. Note
+ * that this will only happen if the session UI has requested input focus in its window;
+ * otherwise, the back key will go to whatever window has focus and do whatever behavior
+ * it normally has there. The default implementation simply calls {@link #hide}.
+ */
+ public void onBackPressed() {
+ hide();
+ }
+
+ /**
+ * Sessions automatically watch for requests that all system UI be closed (such as when
+ * the user presses HOME), which will appear here. The default implementation always
+ * calls {@link #hide}.
+ */
+ public void onCloseSystemDialogs() {
+ hide();
+ }
+
+ /**
+ * Called when the lockscreen was shown.
+ */
+ public void onLockscreenShown() {
+ hide();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ }
+
+ @Override
+ public void onLowMemory() {
+ }
+
+ @Override
+ public void onTrimMemory(int level) {
+ }
+
+ /**
+ * Compute the interesting insets into your UI. The default implementation
+ * sets {@link Insets#contentInsets outInsets.contentInsets.top} to the height
+ * of the window, meaning it should not adjust content underneath. The default touchable
+ * insets are {@link Insets#TOUCHABLE_INSETS_FRAME}, meaning it consumes all touch
+ * events within its window frame.
+ *
+ * @param outInsets Fill in with the current UI insets.
+ */
+ public void onComputeInsets(Insets outInsets) {
+ outInsets.contentInsets.left = 0;
+ outInsets.contentInsets.bottom = 0;
+ outInsets.contentInsets.right = 0;
+ View decor = getWindow().getWindow().getDecorView();
+ outInsets.contentInsets.top = decor.getHeight();
+ outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_FRAME;
+ outInsets.touchableRegion.setEmpty();
+ }
+
+ /**
+ * Called when a task initiated by {@link #startVoiceActivity(android.content.Intent)}
+ * has actually started.
+ *
+ * @param intent The original {@link Intent} supplied to
+ * {@link #startVoiceActivity(android.content.Intent)}.
+ * @param taskId Unique ID of the now running task.
+ */
+ public void onTaskStarted(Intent intent, int taskId) {
+ }
+
+ /**
+ * Called when the last activity of a task initiated by
+ * {@link #startVoiceActivity(android.content.Intent)} has finished. The default
+ * implementation calls {@link #finish()} on the assumption that this represents
+ * the completion of a voice action. You can override the implementation if you would
+ * like a different behavior.
+ *
+ * @param intent The original {@link Intent} supplied to
+ * {@link #startVoiceActivity(android.content.Intent)}.
+ * @param taskId Unique ID of the finished task.
+ */
+ public void onTaskFinished(Intent intent, int taskId) {
+ hide();
+ }
+
+ /**
+ * Request to query for what extended commands the session supports.
+ *
+ * @param commands An array of commands that are being queried.
+ * @return Return an array of booleans indicating which of each entry in the
+ * command array is supported. A true entry in the array indicates the command
+ * is supported; false indicates it is not. The default implementation returns
+ * an array of all false entries.
+ */
+ public boolean[] onGetSupportedCommands(String[] commands) {
+ return new boolean[commands.length];
+ }
+
+ /**
+ * Request to confirm with the user before proceeding with an unrecoverable operation,
+ * corresponding to a {@link android.app.VoiceInteractor.ConfirmationRequest
+ * VoiceInteractor.ConfirmationRequest}.
+ *
+ * @param request The active request.
+ */
+ public void onRequestConfirmation(ConfirmationRequest request) {
+ }
+
+ /**
+ * Request for the user to pick one of N options, corresponding to a
+ * {@link android.app.VoiceInteractor.PickOptionRequest VoiceInteractor.PickOptionRequest}.
+ *
+ * @param request The active request.
+ */
+ public void onRequestPickOption(PickOptionRequest request) {
+ }
+
+ /**
+ * Request to complete the voice interaction session because the voice activity successfully
+ * completed its interaction using voice. Corresponds to
+ * {@link android.app.VoiceInteractor.CompleteVoiceRequest
+ * VoiceInteractor.CompleteVoiceRequest}. The default implementation just sends an empty
+ * confirmation back to allow the activity to exit.
+ *
+ * @param request The active request.
+ */
+ public void onRequestCompleteVoice(CompleteVoiceRequest request) {
+ }
+
+ /**
+ * Request to abort the voice interaction session because the voice activity can not
+ * complete its interaction using voice. Corresponds to
+ * {@link android.app.VoiceInteractor.AbortVoiceRequest
+ * VoiceInteractor.AbortVoiceRequest}. The default implementation just sends an empty
+ * confirmation back to allow the activity to exit.
+ *
+ * @param request The active request.
+ */
+ public void onRequestAbortVoice(AbortVoiceRequest request) {
+ }
+
+ /**
+ * Process an arbitrary extended command from the caller,
+ * corresponding to a {@link android.app.VoiceInteractor.CommandRequest
+ * VoiceInteractor.CommandRequest}.
+ *
+ * @param request The active request.
+ */
+ public void onRequestCommand(CommandRequest request) {
+ }
+
+ /**
+ * Called when the {@link android.app.VoiceInteractor} has asked to cancel a {@link Request}
+ * that was previously delivered to {@link #onRequestConfirmation},
+ * {@link #onRequestPickOption}, {@link #onRequestCompleteVoice}, {@link #onRequestAbortVoice},
+ * or {@link #onRequestCommand}.
+ *
+ * @param request The request that is being canceled.
+ */
+ public void onCancelRequest(Request request) {
+ }
+
+ /**
+ * Print the Service's state into the given stream. This gets invoked by
+ * {@link VoiceInteractionSessionService} when its Service
+ * {@link android.app.Service#dump} method is called.
+ *
+ * @param prefix Text to print at the front of each line.
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param writer The PrintWriter to which you should dump your state. This will be
+ * closed for you after you return.
+ * @param args additional arguments to the dump request.
+ */
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.print(prefix); writer.print("mToken="); writer.println(mToken);
+ writer.print(prefix); writer.print("mTheme=#"); writer.println(Integer.toHexString(mTheme));
+ writer.print(prefix); writer.print("mUiEnabled="); writer.println(mUiEnabled);
+ writer.print(" mInitialized="); writer.println(mInitialized);
+ writer.print(prefix); writer.print("mWindowAdded="); writer.print(mWindowAdded);
+ writer.print(" mWindowVisible="); writer.println(mWindowVisible);
+ writer.print(prefix); writer.print("mWindowWasVisible="); writer.print(mWindowWasVisible);
+ writer.print(" mInShowWindow="); writer.println(mInShowWindow);
+ if (mActiveRequests.size() > 0) {
+ writer.print(prefix); writer.println("Active requests:");
+ String innerPrefix = prefix + " ";
+ for (int i=0; i<mActiveRequests.size(); i++) {
+ Request req = mActiveRequests.valueAt(i);
+ writer.print(prefix); writer.print(" #"); writer.print(i);
+ writer.print(": ");
+ writer.println(req);
+ req.dump(innerPrefix, fd, writer, args);
+
+ }
+ }
+ }
+
+ private SafeResultListener createSafeResultListener(
+ @NonNull Consumer<Bundle> consumer) {
+ synchronized (this) {
+ final SafeResultListener listener = new SafeResultListener(consumer, this);
+ mRemoteCallbacks.put(listener, consumer);
+ return listener;
+ }
+ }
+
+ private Consumer<Bundle> removeSafeResultListener(@NonNull SafeResultListener listener) {
+ synchronized (this) {
+ return mRemoteCallbacks.remove(listener);
+ }
+ }
+
+ /**
+ * Represents assist state captured when this session was started.
+ * It contains the various assist data objects and a reference to
+ * the source activity.
+ */
+ @Immutable
+ public static final class AssistState {
+ private final @NonNull ActivityId mActivityId;
+ private final int mIndex;
+ private final int mCount;
+ private final @Nullable Bundle mData;
+ private final @Nullable AssistStructure mStructure;
+ private final @Nullable AssistContent mContent;
+
+ AssistState(@NonNull ActivityId activityId, @Nullable Bundle data,
+ @Nullable AssistStructure structure, @Nullable AssistContent content,
+ int index, int count) {
+ mActivityId = activityId;
+ mIndex = index;
+ mCount = count;
+ mData = data;
+ mStructure = structure;
+ mContent = content;
+ }
+
+ /**
+ * @return whether the source activity is focused.
+ */
+ public boolean isFocused() {
+ return mIndex == 0;
+ }
+
+ /**
+ * @return the index of the activity that this state is for or -1
+ * if there was no assist data captured.
+ */
+ public @IntRange(from = -1) int getIndex() {
+ return mIndex;
+ }
+
+ /**s
+ * @return the total number of activities for which the assist data is
+ * being returned.
+ */
+ public @IntRange(from = 0) int getCount() {
+ return mCount;
+ }
+
+ /**
+ * @return the id of the source activity
+ */
+ public @NonNull ActivityId getActivityId() {
+ return mActivityId;
+ }
+
+ /**
+ * @return Arbitrary data supplied by the app through
+ * {@link android.app.Activity#onProvideAssistData Activity.onProvideAssistData}.
+ * May be null if assist data has been disabled by the user or device policy.
+ */
+ public @Nullable Bundle getAssistData() {
+ return mData;
+ }
+
+ /**
+ * @return If available, the structure definition of all windows currently
+ * displayed by the app. May be null if assist data has been disabled by the user
+ * or device policy; will be an empty stub if the application has disabled assist
+ * by marking its window as secure.
+ */
+ public @Nullable AssistStructure getAssistStructure() {
+ return mStructure;
+ }
+
+ /**
+ * @return Additional content data supplied by the app through
+ * {@link android.app.Activity#onProvideAssistContent Activity.onProvideAssistContent}.
+ * May be null if assist data has been disabled by the user or device policy; will
+ * not be automatically filled in with data from the app if the app has marked its
+ * window as secure.
+ */
+ public @Nullable AssistContent getAssistContent() {
+ return mContent;
+ }
+ }
+
+ /**
+ * Represents the id of an assist source activity. You can use
+ * {@link #equals(Object)} to compare instances of this class.
+ */
+ public static class ActivityId {
+ private final int mTaskId;
+ private final IBinder mAssistToken;
+
+ ActivityId(int taskId, IBinder assistToken) {
+ mTaskId = taskId;
+ mAssistToken = assistToken;
+ }
+
+ int getTaskId() {
+ return mTaskId;
+ }
+
+ IBinder getAssistToken() {
+ return mAssistToken;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ActivityId that = (ActivityId) o;
+
+ if (mTaskId != that.mTaskId) {
+ return false;
+ }
+ return mAssistToken != null
+ ? mAssistToken.equals(that.mAssistToken)
+ : that.mAssistToken == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mTaskId;
+ result = 31 * result + (mAssistToken != null ? mAssistToken.hashCode() : 0);
+ return result;
+ }
+ }
+
+ private static class SafeResultListener implements RemoteCallback.OnResultListener {
+ private final @NonNull WeakReference<VoiceInteractionSession> mWeakSession;
+
+ SafeResultListener(@NonNull Consumer<Bundle> action,
+ @NonNull VoiceInteractionSession session) {
+ mWeakSession = new WeakReference<>(session);
+ }
+
+ @Override
+ public void onResult(Bundle result) {
+ final VoiceInteractionSession session = mWeakSession.get();
+ if (session != null) {
+ final Consumer<Bundle> consumer = session.removeSafeResultListener(this);
+ if (consumer != null) {
+ consumer.accept(result);
+ }
+ }
+ }
+ }
+}
diff --git a/android/service/voice/VoiceInteractionSessionService.java b/android/service/voice/VoiceInteractionSessionService.java
new file mode 100644
index 0000000..424ff9d
--- /dev/null
+++ b/android/service/voice/VoiceInteractionSessionService.java
@@ -0,0 +1,129 @@
+/**
+ * Copyright (C) 2014 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.voice;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import com.android.internal.app.IVoiceInteractionManagerService;
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * An active voice interaction session, initiated by a {@link VoiceInteractionService}.
+ */
+public abstract class VoiceInteractionSessionService extends Service {
+
+ static final int MSG_NEW_SESSION = 1;
+
+ IVoiceInteractionManagerService mSystemService;
+ VoiceInteractionSession mSession;
+
+ IVoiceInteractionSessionService mInterface = new IVoiceInteractionSessionService.Stub() {
+ public void newSession(IBinder token, Bundle args, int startFlags) {
+ mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(MSG_NEW_SESSION,
+ startFlags, token, args));
+
+ }
+ };
+
+ HandlerCaller mHandlerCaller;
+ final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() {
+ @Override
+ public void executeMessage(Message msg) {
+ SomeArgs args = (SomeArgs)msg.obj;
+ switch (msg.what) {
+ case MSG_NEW_SESSION:
+ doNewSession((IBinder)args.arg1, (Bundle)args.arg2, args.argi1);
+ break;
+ }
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mSystemService = IVoiceInteractionManagerService.Stub.asInterface(
+ ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
+ mHandlerCaller = new HandlerCaller(this, Looper.myLooper(),
+ mHandlerCallerCallback, true);
+ }
+
+ public abstract VoiceInteractionSession onNewSession(Bundle args);
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mInterface.asBinder();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mSession != null) {
+ mSession.onConfigurationChanged(newConfig);
+ }
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ if (mSession != null) {
+ mSession.onLowMemory();
+ }
+ }
+
+ @Override
+ public void onTrimMemory(int level) {
+ super.onTrimMemory(level);
+ if (mSession != null) {
+ mSession.onTrimMemory(level);
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ if (mSession == null) {
+ writer.println("(no active session)");
+ } else {
+ writer.println("VoiceInteractionSession:");
+ mSession.dump(" ", fd, writer, args);
+ }
+ }
+
+ void doNewSession(IBinder token, Bundle args, int startFlags) {
+ if (mSession != null) {
+ mSession.doDestroy();
+ mSession = null;
+ }
+ mSession = onNewSession(args);
+ try {
+ mSystemService.deliverNewSession(token, mSession.mSession, mSession.mInteractor);
+ mSession.doCreate(mSystemService, token);
+ } catch (RemoteException e) {
+ }
+ }
+}
diff --git a/android/service/vr/VrListenerService.java b/android/service/vr/VrListenerService.java
new file mode 100644
index 0000000..2758ace
--- /dev/null
+++ b/android/service/vr/VrListenerService.java
@@ -0,0 +1,174 @@
+/**
+ * Copyright (C) 2016 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.vr;
+
+import android.annotation.NonNull;
+import android.annotation.SdkConstant;
+import android.app.ActivityManager;
+import android.app.Service;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+
+/**
+ * A service that is bound from the system while running in virtual reality (VR) mode.
+ *
+ * <p>To extend this class, you must declare the service in your manifest file with
+ * the {@link android.Manifest.permission#BIND_VR_LISTENER_SERVICE} permission
+ * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
+ * <pre>
+ * <service android:name=".VrListener"
+ * android:label="@string/service_name"
+ * android:permission="android.permission.BIND_VR_LISTENER_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.vr.VrListenerService" />
+ * </intent-filter>
+ * </service>
+ * </pre>
+ *
+ * <p>This service is bound when the system enters VR mode and is unbound when the system leaves VR
+ * mode.</p>
+ * <p>The system will enter VR mode when an application that has previously called
+ * {@link android.app.Activity#setVrModeEnabled} gains user focus. The system will only start this
+ * service if the VR application has specifically targeted this service by specifying
+ * its {@link ComponentName} in the call to {@link android.app.Activity#setVrModeEnabled} and if
+ * this service is installed and enabled in the current user's settings.</p>
+ *
+ * @see android.provider.Settings#ACTION_VR_LISTENER_SETTINGS
+ * @see android.app.Activity#setVrModeEnabled
+ * @see android.R.attr#enableVrMode
+ */
+public abstract class VrListenerService extends Service {
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.service.vr.VrListenerService";
+
+ private final Handler mHandler;
+
+ private static final int MSG_ON_CURRENT_VR_ACTIVITY_CHANGED = 1;
+
+ private final IVrListener.Stub mBinder = new IVrListener.Stub() {
+ @Override
+ public void focusedActivityChanged(
+ ComponentName component, boolean running2dInVr, int pid) {
+ mHandler.obtainMessage(MSG_ON_CURRENT_VR_ACTIVITY_CHANGED, running2dInVr ? 1 : 0,
+ pid, component).sendToTarget();
+ }
+ };
+
+ private final class VrListenerHandler extends Handler {
+ public VrListenerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ON_CURRENT_VR_ACTIVITY_CHANGED: {
+ VrListenerService.this.onCurrentVrActivityChanged(
+ (ComponentName) msg.obj, msg.arg1 == 1, msg.arg2);
+ } break;
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ public VrListenerService() {
+ mHandler = new VrListenerHandler(Looper.getMainLooper());
+ }
+
+ /**
+ * Called when the current activity using VR mode has changed.
+ *
+ * <p>This will be called when this service is initially bound, but is not
+ * guaranteed to be called before onUnbind. In general, this is intended to be used to
+ * determine when user focus has transitioned between two VR activities. If both activities
+ * have declared {@link android.R.attr#enableVrMode} with this service (and this
+ * service is present and enabled), this service will not be unbound during the activity
+ * transition.</p>
+ *
+ * @param component the {@link ComponentName} of the VR activity that the system has
+ * switched to, or null if the system is displaying a 2D activity in VR compatibility mode.
+ *
+ * @see android.app.Activity#setVrModeEnabled
+ * @see android.R.attr#enableVrMode
+ */
+ public void onCurrentVrActivityChanged(ComponentName component) {
+ // Override to implement
+ }
+
+ /**
+ * An extended version of onCurrentVrActivityChanged
+ *
+ * <p>This will be called when this service is initially bound, but is not
+ * guaranteed to be called before onUnbind. In general, this is intended to be used to
+ * determine when user focus has transitioned between two VR activities, or between a
+ * VR activity and a 2D activity. This should be overridden instead of the above
+ * onCurrentVrActivityChanged as that version is deprecated.</p>
+ *
+ * @param component the {@link ComponentName} of the VR activity or the 2D intent.
+ * @param running2dInVr true if the component is a 2D component.
+ * @param pid the process the component is running in.
+ *
+ * @see android.app.Activity#setVrModeEnabled
+ * @see android.R.attr#enableVrMode
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void onCurrentVrActivityChanged(
+ ComponentName component, boolean running2dInVr, int pid) {
+ // Override to implement. Default to old behaviour of sending null for 2D.
+ onCurrentVrActivityChanged(running2dInVr ? null : component);
+ }
+
+ /**
+ * Checks if the given component is enabled in user settings.
+ *
+ * <p>If this component is not enabled in the user's settings, it will not be started when
+ * the system enters VR mode. The user interface for enabling VrListenerService components
+ * can be started by sending the {@link android.provider.Settings#ACTION_VR_LISTENER_SETTINGS}
+ * intent.</p>
+ *
+ * @param context the {@link Context} to use for looking up the requested component.
+ * @param requestedComponent the name of the component that implements
+ * {@link android.service.vr.VrListenerService} to check.
+ *
+ * @return {@code true} if this component is enabled in settings.
+ *
+ * @see android.provider.Settings#ACTION_VR_LISTENER_SETTINGS
+ */
+ public static final boolean isVrModePackageEnabled(@NonNull Context context,
+ @NonNull ComponentName requestedComponent) {
+ ActivityManager am = context.getSystemService(ActivityManager.class);
+ if (am == null) {
+ return false;
+ }
+ return am.isVrModePackageEnabled(requestedComponent);
+ }
+}
diff --git a/android/service/wallpaper/WallpaperService.java b/android/service/wallpaper/WallpaperService.java
new file mode 100644
index 0000000..f944dd7
--- /dev/null
+++ b/android/service/wallpaper/WallpaperService.java
@@ -0,0 +1,1617 @@
+/*
+ * Copyright (C) 2009 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.wallpaper;
+
+import android.annotation.FloatRange;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.app.WallpaperColors;
+import android.app.WallpaperInfo;
+import android.app.WallpaperManager;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.MergedConfiguration;
+import android.view.Display;
+import android.view.DisplayCutout;
+import android.view.Gravity;
+import android.view.IWindowSession;
+import android.view.InputChannel;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.view.BaseIWindow;
+import com.android.internal.view.BaseSurfaceHolder;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+
+/**
+ * A wallpaper service is responsible for showing a live wallpaper behind
+ * applications that would like to sit on top of it. This service object
+ * itself does very little -- its only purpose is to generate instances of
+ * {@link Engine} as needed. Implementing a wallpaper thus
+ * involves subclassing from this, subclassing an Engine implementation,
+ * and implementing {@link #onCreateEngine()} to return a new instance of
+ * your engine.
+ */
+public abstract class WallpaperService extends Service {
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_WALLPAPER} permission so
+ * that other applications can not abuse it.
+ */
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE =
+ "android.service.wallpaper.WallpaperService";
+
+ /**
+ * Name under which a WallpaperService component publishes information
+ * about itself. This meta-data must reference an XML resource containing
+ * a <code><{@link android.R.styleable#Wallpaper wallpaper}></code>
+ * tag.
+ */
+ public static final String SERVICE_META_DATA = "android.service.wallpaper";
+
+ static final String TAG = "WallpaperService";
+ static final boolean DEBUG = false;
+
+ private static final int DO_ATTACH = 10;
+ private static final int DO_DETACH = 20;
+ private static final int DO_SET_DESIRED_SIZE = 30;
+ private static final int DO_SET_DISPLAY_PADDING = 40;
+ private static final int DO_IN_AMBIENT_MODE = 50;
+
+ private static final int MSG_UPDATE_SURFACE = 10000;
+ private static final int MSG_VISIBILITY_CHANGED = 10010;
+ private static final int MSG_WALLPAPER_OFFSETS = 10020;
+ private static final int MSG_WALLPAPER_COMMAND = 10025;
+ @UnsupportedAppUsage
+ private static final int MSG_WINDOW_RESIZED = 10030;
+ private static final int MSG_WINDOW_MOVED = 10035;
+ private static final int MSG_TOUCH_EVENT = 10040;
+ private static final int MSG_REQUEST_WALLPAPER_COLORS = 10050;
+ private static final int MSG_SCALE = 10100;
+
+ private static final int NOTIFY_COLORS_RATE_LIMIT_MS = 1000;
+
+ private final ArrayList<Engine> mActiveEngines
+ = new ArrayList<Engine>();
+
+ static final class WallpaperCommand {
+ String action;
+ int x;
+ int y;
+ int z;
+ Bundle extras;
+ boolean sync;
+ }
+
+ /**
+ * The actual implementation of a wallpaper. A wallpaper service may
+ * have multiple instances running (for example as a real wallpaper
+ * and as a preview), each of which is represented by its own Engine
+ * instance. You must implement {@link WallpaperService#onCreateEngine()}
+ * to return your concrete Engine implementation.
+ */
+ public class Engine {
+ IWallpaperEngineWrapper mIWallpaperEngine;
+
+ // Copies from mIWallpaperEngine.
+ HandlerCaller mCaller;
+ IWallpaperConnection mConnection;
+ IBinder mWindowToken;
+
+ boolean mInitializing = true;
+ boolean mVisible;
+ boolean mReportedVisible;
+ boolean mDestroyed;
+
+ // Current window state.
+ boolean mCreated;
+ boolean mSurfaceCreated;
+ boolean mIsCreating;
+ boolean mDrawingAllowed;
+ boolean mOffsetsChanged;
+ boolean mFixedSizeAllowed;
+ int mWidth;
+ int mHeight;
+ int mFormat;
+ int mType;
+ int mCurWidth;
+ int mCurHeight;
+ float mZoom = 0f;
+ int mWindowFlags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+ int mWindowPrivateFlags =
+ WindowManager.LayoutParams.PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS;
+ int mCurWindowFlags = mWindowFlags;
+ int mCurWindowPrivateFlags = mWindowPrivateFlags;
+ final Rect mVisibleInsets = new Rect();
+ final Rect mWinFrame = new Rect();
+ final Rect mContentInsets = new Rect();
+ final Rect mStableInsets = new Rect();
+ final Rect mDispatchedContentInsets = new Rect();
+ final Rect mDispatchedStableInsets = new Rect();
+ final Rect mFinalSystemInsets = new Rect();
+ final Rect mFinalStableInsets = new Rect();
+ final Rect mBackdropFrame = new Rect();
+ final DisplayCutout.ParcelableWrapper mDisplayCutout =
+ new DisplayCutout.ParcelableWrapper();
+ DisplayCutout mDispatchedDisplayCutout = DisplayCutout.NO_CUTOUT;
+ final InsetsState mInsetsState = new InsetsState();
+ final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0];
+ final MergedConfiguration mMergedConfiguration = new MergedConfiguration();
+ private final Point mSurfaceSize = new Point();
+
+ final WindowManager.LayoutParams mLayout
+ = new WindowManager.LayoutParams();
+ IWindowSession mSession;
+
+ final Object mLock = new Object();
+ boolean mOffsetMessageEnqueued;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ float mPendingXOffset;
+ float mPendingYOffset;
+ float mPendingXOffsetStep;
+ float mPendingYOffsetStep;
+ boolean mPendingSync;
+ MotionEvent mPendingMove;
+ boolean mIsInAmbientMode;
+
+ // Needed for throttling onComputeColors.
+ private long mLastColorInvalidation;
+ private final Runnable mNotifyColorsChanged = this::notifyColorsChanged;
+ private final Supplier<Long> mClockFunction;
+ private final Handler mHandler;
+
+ private Display mDisplay;
+ private Context mDisplayContext;
+ private int mDisplayState;
+
+ SurfaceControl mSurfaceControl = new SurfaceControl();
+
+ // Unused relayout out-param
+ SurfaceControl mTmpSurfaceControl = new SurfaceControl();
+
+ final BaseSurfaceHolder mSurfaceHolder = new BaseSurfaceHolder() {
+ {
+ mRequestedFormat = PixelFormat.RGBX_8888;
+ }
+
+ @Override
+ public boolean onAllowLockCanvas() {
+ return mDrawingAllowed;
+ }
+
+ @Override
+ public void onRelayoutContainer() {
+ Message msg = mCaller.obtainMessage(MSG_UPDATE_SURFACE);
+ mCaller.sendMessage(msg);
+ }
+
+ @Override
+ public void onUpdateSurface() {
+ Message msg = mCaller.obtainMessage(MSG_UPDATE_SURFACE);
+ mCaller.sendMessage(msg);
+ }
+
+ public boolean isCreating() {
+ return mIsCreating;
+ }
+
+ @Override
+ public void setFixedSize(int width, int height) {
+ if (!mFixedSizeAllowed) {
+ // Regular apps can't do this. It can only work for
+ // certain designs of window animations, so you can't
+ // rely on it.
+ throw new UnsupportedOperationException(
+ "Wallpapers currently only support sizing from layout");
+ }
+ super.setFixedSize(width, height);
+ }
+
+ public void setKeepScreenOn(boolean screenOn) {
+ throw new UnsupportedOperationException(
+ "Wallpapers do not support keep screen on");
+ }
+
+ private void prepareToDraw() {
+ if (mDisplayState == Display.STATE_DOZE
+ || mDisplayState == Display.STATE_DOZE_SUSPEND) {
+ try {
+ mSession.pokeDrawLock(mWindow);
+ } catch (RemoteException e) {
+ // System server died, can be ignored.
+ }
+ }
+ }
+
+ @Override
+ public Canvas lockCanvas() {
+ prepareToDraw();
+ return super.lockCanvas();
+ }
+
+ @Override
+ public Canvas lockCanvas(Rect dirty) {
+ prepareToDraw();
+ return super.lockCanvas(dirty);
+ }
+
+ @Override
+ public Canvas lockHardwareCanvas() {
+ prepareToDraw();
+ return super.lockHardwareCanvas();
+ }
+ };
+
+ final class WallpaperInputEventReceiver extends InputEventReceiver {
+ public WallpaperInputEventReceiver(InputChannel inputChannel, Looper looper) {
+ super(inputChannel, looper);
+ }
+
+ @Override
+ public void onInputEvent(InputEvent event) {
+ boolean handled = false;
+ try {
+ if (event instanceof MotionEvent
+ && (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ MotionEvent dup = MotionEvent.obtainNoHistory((MotionEvent)event);
+ dispatchPointer(dup);
+ handled = true;
+ }
+ } finally {
+ finishInputEvent(event, handled);
+ }
+ }
+ }
+ WallpaperInputEventReceiver mInputEventReceiver;
+
+ final BaseIWindow mWindow = new BaseIWindow() {
+ @Override
+ public void resized(Rect frame, Rect contentInsets,
+ Rect visibleInsets, Rect stableInsets, boolean reportDraw,
+ MergedConfiguration mergedConfiguration, Rect backDropRect, boolean forceLayout,
+ boolean alwaysConsumeSystemBars, int displayId,
+ DisplayCutout.ParcelableWrapper displayCutout) {
+ Message msg = mCaller.obtainMessageI(MSG_WINDOW_RESIZED,
+ reportDraw ? 1 : 0);
+ mCaller.sendMessage(msg);
+ }
+
+ @Override
+ public void moved(int newX, int newY) {
+ Message msg = mCaller.obtainMessageII(MSG_WINDOW_MOVED, newX, newY);
+ mCaller.sendMessage(msg);
+ }
+
+ @Override
+ public void dispatchAppVisibility(boolean visible) {
+ // We don't do this in preview mode; we'll let the preview
+ // activity tell us when to run.
+ if (!mIWallpaperEngine.mIsPreview) {
+ Message msg = mCaller.obtainMessageI(MSG_VISIBILITY_CHANGED,
+ visible ? 1 : 0);
+ mCaller.sendMessage(msg);
+ }
+ }
+
+ @Override
+ public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep,
+ float zoom, boolean sync) {
+ synchronized (mLock) {
+ if (DEBUG) Log.v(TAG, "Dispatch wallpaper offsets: " + x + ", " + y);
+ mPendingXOffset = x;
+ mPendingYOffset = y;
+ mPendingXOffsetStep = xStep;
+ mPendingYOffsetStep = yStep;
+ if (sync) {
+ mPendingSync = true;
+ }
+ if (!mOffsetMessageEnqueued) {
+ mOffsetMessageEnqueued = true;
+ Message msg = mCaller.obtainMessage(MSG_WALLPAPER_OFFSETS);
+ mCaller.sendMessage(msg);
+ }
+ Message msg = mCaller.obtainMessageI(MSG_SCALE, Float.floatToIntBits(zoom));
+ mCaller.sendMessage(msg);
+ }
+ }
+
+ @Override
+ public void dispatchWallpaperCommand(String action, int x, int y,
+ int z, Bundle extras, boolean sync) {
+ synchronized (mLock) {
+ if (DEBUG) Log.v(TAG, "Dispatch wallpaper command: " + x + ", " + y);
+ WallpaperCommand cmd = new WallpaperCommand();
+ cmd.action = action;
+ cmd.x = x;
+ cmd.y = y;
+ cmd.z = z;
+ cmd.extras = extras;
+ cmd.sync = sync;
+ Message msg = mCaller.obtainMessage(MSG_WALLPAPER_COMMAND);
+ msg.obj = cmd;
+ mCaller.sendMessage(msg);
+ }
+ }
+ };
+
+ /**
+ * Default constructor
+ */
+ public Engine() {
+ this(SystemClock::elapsedRealtime, Handler.getMain());
+ }
+
+ /**
+ * Constructor used for test purposes.
+ *
+ * @param clockFunction Supplies current times in millis.
+ * @param handler Used for posting/deferring asynchronous calls.
+ * @hide
+ */
+ @VisibleForTesting
+ public Engine(Supplier<Long> clockFunction, Handler handler) {
+ mClockFunction = clockFunction;
+ mHandler = handler;
+ }
+
+ /**
+ * Provides access to the surface in which this wallpaper is drawn.
+ */
+ public SurfaceHolder getSurfaceHolder() {
+ return mSurfaceHolder;
+ }
+
+ /**
+ * Convenience for {@link WallpaperManager#getDesiredMinimumWidth()
+ * WallpaperManager.getDesiredMinimumWidth()}, returning the width
+ * that the system would like this wallpaper to run in.
+ */
+ public int getDesiredMinimumWidth() {
+ return mIWallpaperEngine.mReqWidth;
+ }
+
+ /**
+ * Convenience for {@link WallpaperManager#getDesiredMinimumHeight()
+ * WallpaperManager.getDesiredMinimumHeight()}, returning the height
+ * that the system would like this wallpaper to run in.
+ */
+ public int getDesiredMinimumHeight() {
+ return mIWallpaperEngine.mReqHeight;
+ }
+
+ /**
+ * Return whether the wallpaper is currently visible to the user,
+ * this is the last value supplied to
+ * {@link #onVisibilityChanged(boolean)}.
+ */
+ public boolean isVisible() {
+ return mReportedVisible;
+ }
+
+ /**
+ * Returns true if this engine is running in preview mode -- that is,
+ * it is being shown to the user before they select it as the actual
+ * wallpaper.
+ */
+ public boolean isPreview() {
+ return mIWallpaperEngine.mIsPreview;
+ }
+
+ /**
+ * Returns true if this engine is running in ambient mode -- that is,
+ * it is being shown in low power mode, on always on display.
+ * @hide
+ */
+ @SystemApi
+ public boolean isInAmbientMode() {
+ return mIsInAmbientMode;
+ }
+
+ /**
+ * This will be called when the wallpaper is first started. If true is returned, the system
+ * will zoom in the wallpaper by default and zoom it out as the user interacts,
+ * to create depth. Otherwise, zoom will have to be handled manually
+ * in {@link #onZoomChanged(float)}.
+ *
+ * @hide
+ */
+ public boolean shouldZoomOutWallpaper() {
+ return false;
+ }
+
+ /**
+ * Control whether this wallpaper will receive raw touch events
+ * from the window manager as the user interacts with the window
+ * that is currently displaying the wallpaper. By default they
+ * are turned off. If enabled, the events will be received in
+ * {@link #onTouchEvent(MotionEvent)}.
+ */
+ public void setTouchEventsEnabled(boolean enabled) {
+ mWindowFlags = enabled
+ ? (mWindowFlags&~WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
+ : (mWindowFlags|WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
+ if (mCreated) {
+ updateSurface(false, false, false);
+ }
+ }
+
+ /**
+ * Control whether this wallpaper will receive notifications when the wallpaper
+ * has been scrolled. By default, wallpapers will receive notifications, although
+ * the default static image wallpapers do not. It is a performance optimization to
+ * set this to false.
+ *
+ * @param enabled whether the wallpaper wants to receive offset notifications
+ */
+ public void setOffsetNotificationsEnabled(boolean enabled) {
+ mWindowPrivateFlags = enabled
+ ? (mWindowPrivateFlags |
+ WindowManager.LayoutParams.PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS)
+ : (mWindowPrivateFlags &
+ ~WindowManager.LayoutParams.PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS);
+ if (mCreated) {
+ updateSurface(false, false, false);
+ }
+ }
+
+ /** {@hide} */
+ @UnsupportedAppUsage
+ public void setFixedSizeAllowed(boolean allowed) {
+ mFixedSizeAllowed = allowed;
+ }
+
+ /**
+ * Returns the current scale of the surface
+ * @hide
+ */
+ @VisibleForTesting
+ public float getZoom() {
+ return mZoom;
+ }
+
+ /**
+ * Called once to initialize the engine. After returning, the
+ * engine's surface will be created by the framework.
+ */
+ public void onCreate(SurfaceHolder surfaceHolder) {
+ }
+
+ /**
+ * Called right before the engine is going away. After this the
+ * surface will be destroyed and this Engine object is no longer
+ * valid.
+ */
+ public void onDestroy() {
+ }
+
+ /**
+ * Called to inform you of the wallpaper becoming visible or
+ * hidden. <em>It is very important that a wallpaper only use
+ * CPU while it is visible.</em>.
+ */
+ public void onVisibilityChanged(boolean visible) {
+ }
+
+ /**
+ * Called with the current insets that are in effect for the wallpaper.
+ * This gives you the part of the overall wallpaper surface that will
+ * generally be visible to the user (ignoring position offsets applied to it).
+ *
+ * @param insets Insets to apply.
+ */
+ public void onApplyWindowInsets(WindowInsets insets) {
+ }
+
+ /**
+ * Called as the user performs touch-screen interaction with the
+ * window that is currently showing this wallpaper. Note that the
+ * events you receive here are driven by the actual application the
+ * user is interacting with, so if it is slow you will get fewer
+ * move events.
+ */
+ public void onTouchEvent(MotionEvent event) {
+ }
+
+ /**
+ * Called to inform you of the wallpaper's offsets changing
+ * within its contain, corresponding to the container's
+ * call to {@link WallpaperManager#setWallpaperOffsets(IBinder, float, float)
+ * WallpaperManager.setWallpaperOffsets()}.
+ */
+ public void onOffsetsChanged(float xOffset, float yOffset,
+ float xOffsetStep, float yOffsetStep,
+ int xPixelOffset, int yPixelOffset) {
+ }
+
+ /**
+ * Process a command that was sent to the wallpaper with
+ * {@link WallpaperManager#sendWallpaperCommand}.
+ * The default implementation does nothing, and always returns null
+ * as the result.
+ *
+ * @param action The name of the command to perform. This tells you
+ * what to do and how to interpret the rest of the arguments.
+ * @param x Generic integer parameter.
+ * @param y Generic integer parameter.
+ * @param z Generic integer parameter.
+ * @param extras Any additional parameters.
+ * @param resultRequested If true, the caller is requesting that
+ * a result, appropriate for the command, be returned back.
+ * @return If returning a result, create a Bundle and place the
+ * result data in to it. Otherwise return null.
+ */
+ public Bundle onCommand(String action, int x, int y, int z,
+ Bundle extras, boolean resultRequested) {
+ return null;
+ }
+
+ /**
+ * Called when the device enters or exits ambient mode.
+ *
+ * @param inAmbientMode {@code true} if in ambient mode.
+ * @param animationDuration How long the transition animation to change the ambient state
+ * should run, in milliseconds. If 0 is passed as the argument
+ * here, the state should be switched immediately.
+ *
+ * @see #isInAmbientMode()
+ * @see WallpaperInfo#supportsAmbientMode()
+ * @hide
+ */
+ @SystemApi
+ public void onAmbientModeChanged(boolean inAmbientMode, long animationDuration) {
+ }
+
+ /**
+ * Called when an application has changed the desired virtual size of
+ * the wallpaper.
+ */
+ public void onDesiredSizeChanged(int desiredWidth, int desiredHeight) {
+ }
+
+ /**
+ * Convenience for {@link SurfaceHolder.Callback#surfaceChanged
+ * SurfaceHolder.Callback.surfaceChanged()}.
+ */
+ public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ }
+
+ /**
+ * Convenience for {@link SurfaceHolder.Callback2#surfaceRedrawNeeded
+ * SurfaceHolder.Callback.surfaceRedrawNeeded()}.
+ */
+ public void onSurfaceRedrawNeeded(SurfaceHolder holder) {
+ }
+
+ /**
+ * Convenience for {@link SurfaceHolder.Callback#surfaceCreated
+ * SurfaceHolder.Callback.surfaceCreated()}.
+ */
+ public void onSurfaceCreated(SurfaceHolder holder) {
+ }
+
+ /**
+ * Convenience for {@link SurfaceHolder.Callback#surfaceDestroyed
+ * SurfaceHolder.Callback.surfaceDestroyed()}.
+ */
+ public void onSurfaceDestroyed(SurfaceHolder holder) {
+ }
+
+ /**
+ * Called when the zoom level of the wallpaper changed.
+ * This method will be called with the initial zoom level when the surface is created.
+ *
+ * @param zoom the zoom level, between 0 indicating fully zoomed in and 1 indicating fully
+ * zoomed out.
+ */
+ public void onZoomChanged(@FloatRange(from = 0f, to = 1f) float zoom) {
+ }
+
+ /**
+ * Notifies the engine that wallpaper colors changed significantly.
+ * This will trigger a {@link #onComputeColors()} call.
+ */
+ public void notifyColorsChanged() {
+ final long now = mClockFunction.get();
+ if (now - mLastColorInvalidation < NOTIFY_COLORS_RATE_LIMIT_MS) {
+ Log.w(TAG, "This call has been deferred. You should only call "
+ + "notifyColorsChanged() once every "
+ + (NOTIFY_COLORS_RATE_LIMIT_MS / 1000f) + " seconds.");
+ if (!mHandler.hasCallbacks(mNotifyColorsChanged)) {
+ mHandler.postDelayed(mNotifyColorsChanged, NOTIFY_COLORS_RATE_LIMIT_MS);
+ }
+ return;
+ }
+ mLastColorInvalidation = now;
+ mHandler.removeCallbacks(mNotifyColorsChanged);
+
+ try {
+ final WallpaperColors newColors = onComputeColors();
+ if (mConnection != null) {
+ mConnection.onWallpaperColorsChanged(newColors, mDisplay.getDisplayId());
+ } else {
+ Log.w(TAG, "Can't notify system because wallpaper connection "
+ + "was not established.");
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Can't notify system because wallpaper connection was lost.", e);
+ }
+ }
+
+ /**
+ * Called by the system when it needs to know what colors the wallpaper is using.
+ * You might return null if no color information is available at the moment.
+ * In that case you might want to call {@link #notifyColorsChanged()} when
+ * color information becomes available.
+ * <p>
+ * The simplest way of creating a {@link android.app.WallpaperColors} object is by using
+ * {@link android.app.WallpaperColors#fromBitmap(Bitmap)} or
+ * {@link android.app.WallpaperColors#fromDrawable(Drawable)}, but you can also specify
+ * your main colors by constructing a {@link android.app.WallpaperColors} object manually.
+ *
+ * @return Wallpaper colors.
+ */
+ public @Nullable WallpaperColors onComputeColors() {
+ return null;
+ }
+
+ /**
+ * Sets internal engine state. Only for testing.
+ * @param created {@code true} or {@code false}.
+ * @hide
+ */
+ @VisibleForTesting
+ public void setCreated(boolean created) {
+ mCreated = created;
+ }
+
+ protected void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
+ out.print(prefix); out.print("mInitializing="); out.print(mInitializing);
+ out.print(" mDestroyed="); out.println(mDestroyed);
+ out.print(prefix); out.print("mVisible="); out.print(mVisible);
+ out.print(" mReportedVisible="); out.println(mReportedVisible);
+ out.print(prefix); out.print("mDisplay="); out.println(mDisplay);
+ out.print(prefix); out.print("mCreated="); out.print(mCreated);
+ out.print(" mSurfaceCreated="); out.print(mSurfaceCreated);
+ out.print(" mIsCreating="); out.print(mIsCreating);
+ out.print(" mDrawingAllowed="); out.println(mDrawingAllowed);
+ out.print(prefix); out.print("mWidth="); out.print(mWidth);
+ out.print(" mCurWidth="); out.print(mCurWidth);
+ out.print(" mHeight="); out.print(mHeight);
+ out.print(" mCurHeight="); out.println(mCurHeight);
+ out.print(prefix); out.print("mType="); out.print(mType);
+ out.print(" mWindowFlags="); out.print(mWindowFlags);
+ out.print(" mCurWindowFlags="); out.println(mCurWindowFlags);
+ out.print(prefix); out.print("mWindowPrivateFlags="); out.print(mWindowPrivateFlags);
+ out.print(" mCurWindowPrivateFlags="); out.println(mCurWindowPrivateFlags);
+ out.print(prefix); out.print("mVisibleInsets=");
+ out.print(mVisibleInsets.toShortString());
+ out.print(" mWinFrame="); out.print(mWinFrame.toShortString());
+ out.print(" mContentInsets="); out.println(mContentInsets.toShortString());
+ out.print(prefix); out.print("mConfiguration=");
+ out.println(mMergedConfiguration.getMergedConfiguration());
+ out.print(prefix); out.print("mLayout="); out.println(mLayout);
+ out.print(prefix); out.print("mZoom="); out.println(mZoom);
+ synchronized (mLock) {
+ out.print(prefix); out.print("mPendingXOffset="); out.print(mPendingXOffset);
+ out.print(" mPendingXOffset="); out.println(mPendingXOffset);
+ out.print(prefix); out.print("mPendingXOffsetStep=");
+ out.print(mPendingXOffsetStep);
+ out.print(" mPendingXOffsetStep="); out.println(mPendingXOffsetStep);
+ out.print(prefix); out.print("mOffsetMessageEnqueued=");
+ out.print(mOffsetMessageEnqueued);
+ out.print(" mPendingSync="); out.println(mPendingSync);
+ if (mPendingMove != null) {
+ out.print(prefix); out.print("mPendingMove="); out.println(mPendingMove);
+ }
+ }
+ }
+
+ /**
+ * Set the wallpaper zoom to the given value. This value will be ignored when in ambient
+ * mode (and zoom will be reset to 0).
+ * @hide
+ * @param zoom between 0 and 1 (inclusive) indicating fully zoomed in to fully zoomed out
+ * respectively.
+ */
+ @VisibleForTesting
+ public void setZoom(float zoom) {
+ if (DEBUG) {
+ Log.v(TAG, "set zoom received: " + zoom);
+ }
+ boolean updated = false;
+ synchronized (mLock) {
+ if (DEBUG) {
+ Log.v(TAG, "mZoom: " + mZoom + " updated: " + zoom);
+ }
+ if (mIsInAmbientMode) {
+ mZoom = 0;
+ }
+ if (Float.compare(zoom, mZoom) != 0) {
+ mZoom = zoom;
+ updated = true;
+ }
+ }
+ if (DEBUG) Log.v(TAG, "setZoom updated? " + updated);
+ if (updated && !mDestroyed) {
+ onZoomChanged(mZoom);
+ }
+ }
+
+ private void dispatchPointer(MotionEvent event) {
+ if (event.isTouchEvent()) {
+ synchronized (mLock) {
+ if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ mPendingMove = event;
+ } else {
+ mPendingMove = null;
+ }
+ }
+ Message msg = mCaller.obtainMessageO(MSG_TOUCH_EVENT, event);
+ mCaller.sendMessage(msg);
+ } else {
+ event.recycle();
+ }
+ }
+
+ void updateSurface(boolean forceRelayout, boolean forceReport, boolean redrawNeeded) {
+ if (mDestroyed) {
+ Log.w(TAG, "Ignoring updateSurface: destroyed");
+ }
+
+ boolean fixedSize = false;
+ int myWidth = mSurfaceHolder.getRequestedWidth();
+ if (myWidth <= 0) myWidth = ViewGroup.LayoutParams.MATCH_PARENT;
+ else fixedSize = true;
+ int myHeight = mSurfaceHolder.getRequestedHeight();
+ if (myHeight <= 0) myHeight = ViewGroup.LayoutParams.MATCH_PARENT;
+ else fixedSize = true;
+
+ final boolean creating = !mCreated;
+ final boolean surfaceCreating = !mSurfaceCreated;
+ final boolean formatChanged = mFormat != mSurfaceHolder.getRequestedFormat();
+ boolean sizeChanged = mWidth != myWidth || mHeight != myHeight;
+ boolean insetsChanged = !mCreated;
+ final boolean typeChanged = mType != mSurfaceHolder.getRequestedType();
+ final boolean flagsChanged = mCurWindowFlags != mWindowFlags ||
+ mCurWindowPrivateFlags != mWindowPrivateFlags;
+ if (forceRelayout || creating || surfaceCreating || formatChanged || sizeChanged
+ || typeChanged || flagsChanged || redrawNeeded
+ || !mIWallpaperEngine.mShownReported) {
+
+ if (DEBUG) Log.v(TAG, "Changes: creating=" + creating
+ + " format=" + formatChanged + " size=" + sizeChanged);
+
+ try {
+ mWidth = myWidth;
+ mHeight = myHeight;
+ mFormat = mSurfaceHolder.getRequestedFormat();
+ mType = mSurfaceHolder.getRequestedType();
+
+ mLayout.x = 0;
+ mLayout.y = 0;
+
+ mLayout.width = myWidth;
+ mLayout.height = myHeight;
+ mLayout.format = mFormat;
+
+ mCurWindowFlags = mWindowFlags;
+ mLayout.flags = mWindowFlags
+ | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ mCurWindowPrivateFlags = mWindowPrivateFlags;
+ mLayout.privateFlags = mWindowPrivateFlags;
+
+ mLayout.memoryType = mType;
+ mLayout.token = mWindowToken;
+
+ if (!mCreated) {
+ // Retrieve watch round info
+ TypedArray windowStyle = obtainStyledAttributes(
+ com.android.internal.R.styleable.Window);
+ windowStyle.recycle();
+
+ // Add window
+ mLayout.type = mIWallpaperEngine.mWindowType;
+ mLayout.gravity = Gravity.START|Gravity.TOP;
+ mLayout.setFitInsetsTypes(0 /* types */);
+ mLayout.setTitle(WallpaperService.this.getClass().getName());
+ mLayout.windowAnimations =
+ com.android.internal.R.style.Animation_Wallpaper;
+ InputChannel inputChannel = new InputChannel();
+
+ if (mSession.addToDisplay(mWindow, mWindow.mSeq, mLayout, View.VISIBLE,
+ mDisplay.getDisplayId(), mWinFrame, mContentInsets, mStableInsets,
+ mDisplayCutout, inputChannel,
+ mInsetsState, mTempControls) < 0) {
+ Log.w(TAG, "Failed to add window while updating wallpaper surface.");
+ return;
+ }
+ mSession.setShouldZoomOutWallpaper(mWindow, shouldZoomOutWallpaper());
+ mCreated = true;
+
+ mInputEventReceiver = new WallpaperInputEventReceiver(
+ inputChannel, Looper.myLooper());
+ }
+
+ mSurfaceHolder.mSurfaceLock.lock();
+ mDrawingAllowed = true;
+
+ if (!fixedSize) {
+ mLayout.surfaceInsets.set(mIWallpaperEngine.mDisplayPadding);
+ } else {
+ mLayout.surfaceInsets.set(0, 0, 0, 0);
+ }
+
+ final int relayoutResult = mSession.relayout(
+ mWindow, mWindow.mSeq, mLayout, mWidth, mHeight,
+ View.VISIBLE, 0, -1, mWinFrame, mContentInsets,
+ mVisibleInsets, mStableInsets, mBackdropFrame,
+ mDisplayCutout, mMergedConfiguration, mSurfaceControl,
+ mInsetsState, mTempControls, mSurfaceSize, mTmpSurfaceControl);
+ if (mSurfaceControl.isValid()) {
+ mSurfaceHolder.mSurface.copyFrom(mSurfaceControl);
+ mSurfaceControl.release();
+ }
+
+ if (DEBUG) Log.v(TAG, "New surface: " + mSurfaceHolder.mSurface
+ + ", frame=" + mWinFrame);
+
+ int w = mWinFrame.width();
+ int h = mWinFrame.height();
+
+ if (!fixedSize) {
+ final Rect padding = mIWallpaperEngine.mDisplayPadding;
+ w += padding.left + padding.right;
+ h += padding.top + padding.bottom;
+ mContentInsets.left += padding.left;
+ mContentInsets.top += padding.top;
+ mContentInsets.right += padding.right;
+ mContentInsets.bottom += padding.bottom;
+ mStableInsets.left += padding.left;
+ mStableInsets.top += padding.top;
+ mStableInsets.right += padding.right;
+ mStableInsets.bottom += padding.bottom;
+ mDisplayCutout.set(mDisplayCutout.get().inset(-padding.left, -padding.top,
+ -padding.right, -padding.bottom));
+ } else {
+ w = myWidth;
+ h = myHeight;
+ }
+
+ if (mCurWidth != w) {
+ sizeChanged = true;
+ mCurWidth = w;
+ }
+ if (mCurHeight != h) {
+ sizeChanged = true;
+ mCurHeight = h;
+ }
+
+ if (DEBUG) {
+ Log.v(TAG, "Wallpaper size has changed: (" + mCurWidth + ", " + mCurHeight);
+ }
+
+ insetsChanged |= !mDispatchedContentInsets.equals(mContentInsets);
+ insetsChanged |= !mDispatchedStableInsets.equals(mStableInsets);
+ insetsChanged |= !mDispatchedDisplayCutout.equals(mDisplayCutout.get());
+
+ mSurfaceHolder.setSurfaceFrameSize(w, h);
+ mSurfaceHolder.mSurfaceLock.unlock();
+
+ if (!mSurfaceHolder.mSurface.isValid()) {
+ reportSurfaceDestroyed();
+ if (DEBUG) Log.v(TAG, "Layout: Surface destroyed");
+ return;
+ }
+
+ boolean didSurface = false;
+
+ try {
+ mSurfaceHolder.ungetCallbacks();
+
+ if (surfaceCreating) {
+ mIsCreating = true;
+ didSurface = true;
+ if (DEBUG) Log.v(TAG, "onSurfaceCreated("
+ + mSurfaceHolder + "): " + this);
+ onSurfaceCreated(mSurfaceHolder);
+ SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
+ if (callbacks != null) {
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceCreated(mSurfaceHolder);
+ }
+ }
+ }
+
+ redrawNeeded |= creating || (relayoutResult
+ & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0;
+
+ if (forceReport || creating || surfaceCreating
+ || formatChanged || sizeChanged) {
+ if (DEBUG) {
+ RuntimeException e = new RuntimeException();
+ e.fillInStackTrace();
+ Log.w(TAG, "forceReport=" + forceReport + " creating=" + creating
+ + " formatChanged=" + formatChanged
+ + " sizeChanged=" + sizeChanged, e);
+ }
+ if (DEBUG) Log.v(TAG, "onSurfaceChanged("
+ + mSurfaceHolder + ", " + mFormat
+ + ", " + mCurWidth + ", " + mCurHeight
+ + "): " + this);
+ didSurface = true;
+ onSurfaceChanged(mSurfaceHolder, mFormat,
+ mCurWidth, mCurHeight);
+ SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
+ if (callbacks != null) {
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceChanged(mSurfaceHolder, mFormat,
+ mCurWidth, mCurHeight);
+ }
+ }
+ }
+
+ if (insetsChanged) {
+ mDispatchedContentInsets.set(mContentInsets);
+ mDispatchedStableInsets.set(mStableInsets);
+ mDispatchedDisplayCutout = mDisplayCutout.get();
+ mFinalStableInsets.set(mDispatchedStableInsets);
+ WindowInsets insets = new WindowInsets(mFinalSystemInsets,
+ mFinalStableInsets,
+ getResources().getConfiguration().isScreenRound(), false,
+ mDispatchedDisplayCutout);
+ if (DEBUG) {
+ Log.v(TAG, "dispatching insets=" + insets);
+ }
+ onApplyWindowInsets(insets);
+ }
+
+ if (redrawNeeded) {
+ onSurfaceRedrawNeeded(mSurfaceHolder);
+ SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
+ if (callbacks != null) {
+ for (SurfaceHolder.Callback c : callbacks) {
+ if (c instanceof SurfaceHolder.Callback2) {
+ ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded(
+ mSurfaceHolder);
+ }
+ }
+ }
+ }
+
+ if (didSurface && !mReportedVisible) {
+ // This wallpaper is currently invisible, but its
+ // surface has changed. At this point let's tell it
+ // again that it is invisible in case the report about
+ // the surface caused it to start running. We really
+ // don't want wallpapers running when not visible.
+ if (mIsCreating) {
+ // Some wallpapers will ignore this call if they
+ // had previously been told they were invisble,
+ // so if we are creating a new surface then toggle
+ // the state to get them to notice.
+ if (DEBUG) Log.v(TAG, "onVisibilityChanged(true) at surface: "
+ + this);
+ onVisibilityChanged(true);
+ }
+ if (DEBUG) Log.v(TAG, "onVisibilityChanged(false) at surface: "
+ + this);
+ onVisibilityChanged(false);
+ }
+
+ } finally {
+ mIsCreating = false;
+ mSurfaceCreated = true;
+ if (redrawNeeded) {
+ mSession.finishDrawing(mWindow, null /* postDrawTransaction */);
+ }
+ mIWallpaperEngine.reportShown();
+ }
+ } catch (RemoteException ex) {
+ }
+ if (DEBUG) Log.v(
+ TAG, "Layout: x=" + mLayout.x + " y=" + mLayout.y +
+ " w=" + mLayout.width + " h=" + mLayout.height);
+ }
+ }
+
+ void attach(IWallpaperEngineWrapper wrapper) {
+ if (DEBUG) Log.v(TAG, "attach: " + this + " wrapper=" + wrapper);
+ if (mDestroyed) {
+ return;
+ }
+
+ mIWallpaperEngine = wrapper;
+ mCaller = wrapper.mCaller;
+ mConnection = wrapper.mConnection;
+ mWindowToken = wrapper.mWindowToken;
+ mSurfaceHolder.setSizeFromLayout();
+ mInitializing = true;
+ mSession = WindowManagerGlobal.getWindowSession();
+
+ mWindow.setSession(mSession);
+
+ mLayout.packageName = getPackageName();
+ mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener,
+ mCaller.getHandler());
+ mDisplay = mIWallpaperEngine.mDisplay;
+ mDisplayContext = createDisplayContext(mDisplay);
+ mDisplayState = mDisplay.getState();
+
+ if (DEBUG) Log.v(TAG, "onCreate(): " + this);
+ onCreate(mSurfaceHolder);
+
+ mInitializing = false;
+
+ mReportedVisible = false;
+ updateSurface(false, false, false);
+ }
+
+ /**
+ * The {@link Context} with resources that match the current display the wallpaper is on.
+ * For multiple display environment, multiple engines can be created to render on each
+ * display, but these displays may have different densities. Use this context to get the
+ * corresponding resources for currently display, avoiding the context of the service.
+ * <p>
+ * The display context will never be {@code null} after
+ * {@link Engine#onCreate(SurfaceHolder)} has been called.
+ *
+ * @return A {@link Context} for current display.
+ */
+ @Nullable
+ public Context getDisplayContext() {
+ return mDisplayContext;
+ }
+
+ /**
+ * Executes life cycle event and updates internal ambient mode state based on
+ * message sent from handler.
+ *
+ * @param inAmbientMode {@code true} if in ambient mode.
+ * @param animationDuration For how long the transition will last, in ms.
+ * @hide
+ */
+ @VisibleForTesting
+ public void doAmbientModeChanged(boolean inAmbientMode, long animationDuration) {
+ if (!mDestroyed) {
+ if (DEBUG) {
+ Log.v(TAG, "onAmbientModeChanged(" + inAmbientMode + ", "
+ + animationDuration + "): " + this);
+ }
+ mIsInAmbientMode = inAmbientMode;
+ if (mCreated) {
+ onAmbientModeChanged(inAmbientMode, animationDuration);
+ }
+ }
+ }
+
+ void doDesiredSizeChanged(int desiredWidth, int desiredHeight) {
+ if (!mDestroyed) {
+ if (DEBUG) Log.v(TAG, "onDesiredSizeChanged("
+ + desiredWidth + "," + desiredHeight + "): " + this);
+ mIWallpaperEngine.mReqWidth = desiredWidth;
+ mIWallpaperEngine.mReqHeight = desiredHeight;
+ onDesiredSizeChanged(desiredWidth, desiredHeight);
+ doOffsetsChanged(true);
+ }
+ }
+
+ void doDisplayPaddingChanged(Rect padding) {
+ if (!mDestroyed) {
+ if (DEBUG) Log.v(TAG, "onDisplayPaddingChanged(" + padding + "): " + this);
+ if (!mIWallpaperEngine.mDisplayPadding.equals(padding)) {
+ mIWallpaperEngine.mDisplayPadding.set(padding);
+ updateSurface(true, false, false);
+ }
+ }
+ }
+
+ void doVisibilityChanged(boolean visible) {
+ if (!mDestroyed) {
+ mVisible = visible;
+ reportVisibility();
+ }
+ }
+
+ void reportVisibility() {
+ if (!mDestroyed) {
+ mDisplayState = mDisplay == null ? Display.STATE_UNKNOWN : mDisplay.getState();
+ boolean visible = mVisible && mDisplayState != Display.STATE_OFF;
+ if (mReportedVisible != visible) {
+ mReportedVisible = visible;
+ if (DEBUG) Log.v(TAG, "onVisibilityChanged(" + visible
+ + "): " + this);
+ if (visible) {
+ // If becoming visible, in preview mode the surface
+ // may have been destroyed so now we need to make
+ // sure it is re-created.
+ doOffsetsChanged(false);
+ updateSurface(false, false, false);
+ }
+ onVisibilityChanged(visible);
+ }
+ }
+ }
+
+ void doOffsetsChanged(boolean always) {
+ if (mDestroyed) {
+ return;
+ }
+
+ if (!always && !mOffsetsChanged) {
+ return;
+ }
+
+ float xOffset;
+ float yOffset;
+ float xOffsetStep;
+ float yOffsetStep;
+ boolean sync;
+ synchronized (mLock) {
+ xOffset = mPendingXOffset;
+ yOffset = mPendingYOffset;
+ xOffsetStep = mPendingXOffsetStep;
+ yOffsetStep = mPendingYOffsetStep;
+ sync = mPendingSync;
+ mPendingSync = false;
+ mOffsetMessageEnqueued = false;
+ }
+
+ if (mSurfaceCreated) {
+ if (mReportedVisible) {
+ if (DEBUG) Log.v(TAG, "Offsets change in " + this
+ + ": " + xOffset + "," + yOffset);
+ final int availw = mIWallpaperEngine.mReqWidth-mCurWidth;
+ final int xPixels = availw > 0 ? -(int)(availw*xOffset+.5f) : 0;
+ final int availh = mIWallpaperEngine.mReqHeight-mCurHeight;
+ final int yPixels = availh > 0 ? -(int)(availh*yOffset+.5f) : 0;
+ onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixels, yPixels);
+ } else {
+ mOffsetsChanged = true;
+ }
+ }
+
+ if (sync) {
+ try {
+ if (DEBUG) Log.v(TAG, "Reporting offsets change complete");
+ mSession.wallpaperOffsetsComplete(mWindow.asBinder());
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ void doCommand(WallpaperCommand cmd) {
+ Bundle result;
+ if (!mDestroyed) {
+ result = onCommand(cmd.action, cmd.x, cmd.y, cmd.z,
+ cmd.extras, cmd.sync);
+ } else {
+ result = null;
+ }
+ if (cmd.sync) {
+ try {
+ if (DEBUG) Log.v(TAG, "Reporting command complete");
+ mSession.wallpaperCommandComplete(mWindow.asBinder(), result);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ void reportSurfaceDestroyed() {
+ if (mSurfaceCreated) {
+ mSurfaceCreated = false;
+ mSurfaceHolder.ungetCallbacks();
+ SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
+ if (callbacks != null) {
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceDestroyed(mSurfaceHolder);
+ }
+ }
+ if (DEBUG) Log.v(TAG, "onSurfaceDestroyed("
+ + mSurfaceHolder + "): " + this);
+ onSurfaceDestroyed(mSurfaceHolder);
+ }
+ }
+
+ void detach() {
+ if (mDestroyed) {
+ return;
+ }
+
+ mDestroyed = true;
+
+ if (mIWallpaperEngine.mDisplayManager != null) {
+ mIWallpaperEngine.mDisplayManager.unregisterDisplayListener(mDisplayListener);
+ }
+
+ if (mVisible) {
+ mVisible = false;
+ if (DEBUG) Log.v(TAG, "onVisibilityChanged(false): " + this);
+ onVisibilityChanged(false);
+ }
+
+ reportSurfaceDestroyed();
+
+ if (DEBUG) Log.v(TAG, "onDestroy(): " + this);
+ onDestroy();
+
+ if (mCreated) {
+ try {
+ if (DEBUG) Log.v(TAG, "Removing window and destroying surface "
+ + mSurfaceHolder.getSurface() + " of: " + this);
+
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+
+ mSession.remove(mWindow);
+ } catch (RemoteException e) {
+ }
+ mSurfaceHolder.mSurface.release();
+ mCreated = false;
+ }
+ }
+
+ private final DisplayListener mDisplayListener = new DisplayListener() {
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (mDisplay.getDisplayId() == displayId) {
+ reportVisibility();
+ }
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {
+ }
+ };
+ }
+
+ class IWallpaperEngineWrapper extends IWallpaperEngine.Stub
+ implements HandlerCaller.Callback {
+ private final HandlerCaller mCaller;
+
+ final IWallpaperConnection mConnection;
+ final IBinder mWindowToken;
+ final int mWindowType;
+ final boolean mIsPreview;
+ boolean mShownReported;
+ int mReqWidth;
+ int mReqHeight;
+ final Rect mDisplayPadding = new Rect();
+ final int mDisplayId;
+ final DisplayManager mDisplayManager;
+ final Display mDisplay;
+ private final AtomicBoolean mDetached = new AtomicBoolean();
+
+ Engine mEngine;
+
+ IWallpaperEngineWrapper(WallpaperService context,
+ IWallpaperConnection conn, IBinder windowToken,
+ int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding,
+ int displayId) {
+ mCaller = new HandlerCaller(context, context.getMainLooper(), this, true);
+ mConnection = conn;
+ mWindowToken = windowToken;
+ mWindowType = windowType;
+ mIsPreview = isPreview;
+ mReqWidth = reqWidth;
+ mReqHeight = reqHeight;
+ mDisplayPadding.set(padding);
+ mDisplayId = displayId;
+
+ // Create a display context before onCreateEngine.
+ mDisplayManager = getSystemService(DisplayManager.class);
+ mDisplay = mDisplayManager.getDisplay(mDisplayId);
+
+ if (mDisplay == null) {
+ // Ignore this engine.
+ throw new IllegalArgumentException("Cannot find display with id" + mDisplayId);
+ }
+ Message msg = mCaller.obtainMessage(DO_ATTACH);
+ mCaller.sendMessage(msg);
+ }
+
+ public void setDesiredSize(int width, int height) {
+ Message msg = mCaller.obtainMessageII(DO_SET_DESIRED_SIZE, width, height);
+ mCaller.sendMessage(msg);
+ }
+
+ public void setDisplayPadding(Rect padding) {
+ Message msg = mCaller.obtainMessageO(DO_SET_DISPLAY_PADDING, padding);
+ mCaller.sendMessage(msg);
+ }
+
+ public void setVisibility(boolean visible) {
+ Message msg = mCaller.obtainMessageI(MSG_VISIBILITY_CHANGED,
+ visible ? 1 : 0);
+ mCaller.sendMessage(msg);
+ }
+
+ @Override
+ public void setInAmbientMode(boolean inAmbientDisplay, long animationDuration)
+ throws RemoteException {
+ Message msg = mCaller.obtainMessageIO(DO_IN_AMBIENT_MODE, inAmbientDisplay ? 1 : 0,
+ animationDuration);
+ mCaller.sendMessage(msg);
+ }
+
+ public void dispatchPointer(MotionEvent event) {
+ if (mEngine != null) {
+ mEngine.dispatchPointer(event);
+ } else {
+ event.recycle();
+ }
+ }
+
+ public void dispatchWallpaperCommand(String action, int x, int y,
+ int z, Bundle extras) {
+ if (mEngine != null) {
+ mEngine.mWindow.dispatchWallpaperCommand(action, x, y, z, extras, false);
+ }
+ }
+
+ public void setZoomOut(float scale) {
+ Message msg = mCaller.obtainMessageI(MSG_SCALE, Float.floatToIntBits(scale));
+ mCaller.sendMessage(msg);
+ }
+
+ public void reportShown() {
+ if (!mShownReported) {
+ mShownReported = true;
+ try {
+ mConnection.engineShown(this);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Wallpaper host disappeared", e);
+ return;
+ }
+ }
+ }
+
+ public void requestWallpaperColors() {
+ Message msg = mCaller.obtainMessage(MSG_REQUEST_WALLPAPER_COLORS);
+ mCaller.sendMessage(msg);
+ }
+
+ public void destroy() {
+ Message msg = mCaller.obtainMessage(DO_DETACH);
+ mCaller.sendMessage(msg);
+ }
+
+ public void detach() {
+ mDetached.set(true);
+ }
+
+ private void doDetachEngine() {
+ mActiveEngines.remove(mEngine);
+ mEngine.detach();
+ }
+
+ @Override
+ public void executeMessage(Message message) {
+ if (mDetached.get()) {
+ if (mActiveEngines.contains(mEngine)) {
+ doDetachEngine();
+ }
+ return;
+ }
+ switch (message.what) {
+ case DO_ATTACH: {
+ try {
+ mConnection.attachEngine(this, mDisplayId);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Wallpaper host disappeared", e);
+ return;
+ }
+ Engine engine = onCreateEngine();
+ mEngine = engine;
+ mActiveEngines.add(engine);
+ engine.attach(this);
+ return;
+ }
+ case DO_DETACH: {
+ doDetachEngine();
+ return;
+ }
+ case DO_SET_DESIRED_SIZE: {
+ mEngine.doDesiredSizeChanged(message.arg1, message.arg2);
+ return;
+ }
+ case DO_SET_DISPLAY_PADDING: {
+ mEngine.doDisplayPaddingChanged((Rect) message.obj);
+ return;
+ }
+ case DO_IN_AMBIENT_MODE: {
+ mEngine.doAmbientModeChanged(message.arg1 != 0, (Long) message.obj);
+ return;
+ }
+ case MSG_UPDATE_SURFACE:
+ mEngine.updateSurface(true, false, false);
+ break;
+ case MSG_SCALE:
+ mEngine.setZoom(Float.intBitsToFloat(message.arg1));
+ break;
+ case MSG_VISIBILITY_CHANGED:
+ if (DEBUG) Log.v(TAG, "Visibility change in " + mEngine
+ + ": " + message.arg1);
+ mEngine.doVisibilityChanged(message.arg1 != 0);
+ break;
+ case MSG_WALLPAPER_OFFSETS: {
+ mEngine.doOffsetsChanged(true);
+ } break;
+ case MSG_WALLPAPER_COMMAND: {
+ WallpaperCommand cmd = (WallpaperCommand)message.obj;
+ mEngine.doCommand(cmd);
+ } break;
+ case MSG_WINDOW_RESIZED: {
+ final boolean reportDraw = message.arg1 != 0;
+ mEngine.updateSurface(true, false, reportDraw);
+ mEngine.doOffsetsChanged(true);
+ } break;
+ case MSG_WINDOW_MOVED: {
+ // Do nothing. What does it mean for a Wallpaper to move?
+ } break;
+ case MSG_TOUCH_EVENT: {
+ boolean skip = false;
+ MotionEvent ev = (MotionEvent)message.obj;
+ if (ev.getAction() == MotionEvent.ACTION_MOVE) {
+ synchronized (mEngine.mLock) {
+ if (mEngine.mPendingMove == ev) {
+ mEngine.mPendingMove = null;
+ } else {
+ // this is not the motion event we are looking for....
+ skip = true;
+ }
+ }
+ }
+ if (!skip) {
+ if (DEBUG) Log.v(TAG, "Delivering touch event: " + ev);
+ mEngine.onTouchEvent(ev);
+ }
+ ev.recycle();
+ } break;
+ case MSG_REQUEST_WALLPAPER_COLORS: {
+ if (mConnection == null) {
+ break;
+ }
+ try {
+ mConnection.onWallpaperColorsChanged(mEngine.onComputeColors(), mDisplayId);
+ } catch (RemoteException e) {
+ // Connection went away, nothing to do in here.
+ }
+ } break;
+ default :
+ Log.w(TAG, "Unknown message type " + message.what);
+ }
+ }
+ }
+
+ /**
+ * Implements the internal {@link IWallpaperService} interface to convert
+ * incoming calls to it back to calls on an {@link WallpaperService}.
+ */
+ class IWallpaperServiceWrapper extends IWallpaperService.Stub {
+ private final WallpaperService mTarget;
+ private IWallpaperEngineWrapper mEngineWrapper;
+
+ public IWallpaperServiceWrapper(WallpaperService context) {
+ mTarget = context;
+ }
+
+ @Override
+ public void attach(IWallpaperConnection conn, IBinder windowToken,
+ int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding,
+ int displayId) {
+ mEngineWrapper = new IWallpaperEngineWrapper(mTarget, conn, windowToken,
+ windowType, isPreview, reqWidth, reqHeight, padding, displayId);
+ }
+
+ @Override
+ public void detach() {
+ mEngineWrapper.detach();
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ for (int i=0; i<mActiveEngines.size(); i++) {
+ mActiveEngines.get(i).detach();
+ }
+ mActiveEngines.clear();
+ }
+
+ /**
+ * Implement to return the implementation of the internal accessibility
+ * service interface. Subclasses should not override.
+ */
+ @Override
+ public final IBinder onBind(Intent intent) {
+ return new IWallpaperServiceWrapper(this);
+ }
+
+ /**
+ * Must be implemented to return a new instance of the wallpaper's engine.
+ * Note that multiple instances may be active at the same time, such as
+ * when the wallpaper is currently set as the active wallpaper and the user
+ * is in the wallpaper picker viewing a preview of it as well.
+ */
+ public abstract Engine onCreateEngine();
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter out, String[] args) {
+ out.print("State of wallpaper "); out.print(this); out.println(":");
+ for (int i=0; i<mActiveEngines.size(); i++) {
+ Engine engine = mActiveEngines.get(i);
+ out.print(" Engine "); out.print(engine); out.println(":");
+ engine.dump(" ", fd, out, args);
+ }
+ }
+}
diff --git a/android/service/wallpaper/WallpaperSettingsActivity.java b/android/service/wallpaper/WallpaperSettingsActivity.java
new file mode 100644
index 0000000..aca336f
--- /dev/null
+++ b/android/service/wallpaper/WallpaperSettingsActivity.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.wallpaper;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+/**
+ * Base class for activities that will be used to configure the settings of
+ * a wallpaper. You should derive from this class to allow it to select the
+ * proper theme of the activity depending on how it is being used.
+ * @hide
+ */
+public class WallpaperSettingsActivity extends PreferenceActivity {
+ /**
+ * This boolean extra in the launch intent indicates that the settings
+ * are being used while the wallpaper is in preview mode.
+ */
+ final public static String EXTRA_PREVIEW_MODE
+ = "android.service.wallpaper.PREVIEW_MODE";
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ if (false) {
+ Resources.Theme theme = getTheme();
+ if (getIntent().getBooleanExtra(EXTRA_PREVIEW_MODE, false)) {
+ theme.applyStyle(com.android.internal.R.style.PreviewWallpaperSettings, true);
+ } else {
+ theme.applyStyle(com.android.internal.R.style.ActiveWallpaperSettings, true);
+ }
+ }
+ super.onCreate(icicle);
+ }
+}
diff --git a/android/service/watchdog/ExplicitHealthCheckService.java b/android/service/watchdog/ExplicitHealthCheckService.java
new file mode 100644
index 0000000..b1647fe
--- /dev/null
+++ b/android/service/watchdog/ExplicitHealthCheckService.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2019 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.watchdog;
+
+import static android.os.Parcelable.Creator;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A service to provide packages supporting explicit health checks and route checks to these
+ * packages on behalf of the package watchdog.
+ *
+ * <p>To extend this class, you must declare the service in your manifest file with the
+ * {@link android.Manifest.permission#BIND_EXPLICIT_HEALTH_CHECK_SERVICE} permission,
+ * and include an intent filter with the {@link #SERVICE_INTERFACE} action. In adddition,
+ * your implementation must live in
+ * {@link PackageManager#getServicesSystemSharedLibraryPackageName()}.
+ * For example:</p>
+ * <pre>
+ * <service android:name=".FooExplicitHealthCheckService"
+ * android:exported="true"
+ * android:priority="100"
+ * android:permission="android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.watchdog.ExplicitHealthCheckService" />
+ * </intent-filter>
+ * </service>
+ * </pre>
+ * @hide
+ */
+@TestApi
+@SystemApi
+public abstract class ExplicitHealthCheckService extends Service {
+
+ private static final String TAG = "ExplicitHealthCheckService";
+
+ /**
+ * {@link Bundle} key for a {@link List} of {@link PackageConfig} value.
+ *
+ * {@hide}
+ */
+ public static final String EXTRA_SUPPORTED_PACKAGES =
+ "android.service.watchdog.extra.supported_packages";
+
+ /**
+ * {@link Bundle} key for a {@link List} of {@link String} value.
+ *
+ * {@hide}
+ */
+ public static final String EXTRA_REQUESTED_PACKAGES =
+ "android.service.watchdog.extra.requested_packages";
+
+ /**
+ * {@link Bundle} key for a {@link String} value.
+ *
+ * {@hide}
+ */
+ public static final String EXTRA_HEALTH_CHECK_PASSED_PACKAGE =
+ "android.service.watchdog.extra.health_check_passed_package";
+
+ /**
+ * The Intent action that a service must respond to. Add it to the intent filter of the service
+ * in its manifest.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE =
+ "android.service.watchdog.ExplicitHealthCheckService";
+
+ /**
+ * The permission that a service must require to ensure that only Android system can bind to it.
+ * If this permission is not enforced in the AndroidManifest of the service, the system will
+ * skip that service.
+ */
+ public static final String BIND_PERMISSION =
+ "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE";
+
+ private final ExplicitHealthCheckServiceWrapper mWrapper =
+ new ExplicitHealthCheckServiceWrapper();
+
+ /**
+ * Called when the system requests an explicit health check for {@code packageName}.
+ *
+ * <p> When {@code packageName} passes the check, implementors should call
+ * {@link #notifyHealthCheckPassed} to inform the system.
+ *
+ * <p> It could take many hours before a {@code packageName} passes a check and implementors
+ * should never drop requests unless {@link onCancel} is called or the service dies.
+ *
+ * <p> Requests should not be queued and additional calls while expecting a result for
+ * {@code packageName} should have no effect.
+ */
+ public abstract void onRequestHealthCheck(@NonNull String packageName);
+
+ /**
+ * Called when the system cancels the explicit health check request for {@code packageName}.
+ * Should do nothing if there are is no active request for {@code packageName}.
+ */
+ public abstract void onCancelHealthCheck(@NonNull String packageName);
+
+ /**
+ * Called when the system requests for all the packages supporting explicit health checks. The
+ * system may request an explicit health check for any of these packages with
+ * {@link #onRequestHealthCheck}.
+ *
+ * @return all packages supporting explicit health checks
+ */
+ @NonNull public abstract List<PackageConfig> onGetSupportedPackages();
+
+ /**
+ * Called when the system requests for all the packages that it has currently requested
+ * an explicit health check for.
+ *
+ * @return all packages expecting an explicit health check result
+ */
+ @NonNull public abstract List<String> onGetRequestedPackages();
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
+ @Nullable private RemoteCallback mCallback;
+
+ @Override
+ @NonNull
+ public final IBinder onBind(@NonNull Intent intent) {
+ return mWrapper;
+ }
+
+ /**
+ * Sets {@link RemoteCallback}, for testing purpose.
+ *
+ * @hide
+ */
+ @TestApi
+ public void setCallback(@Nullable RemoteCallback callback) {
+ mCallback = callback;
+ }
+ /**
+ * Implementors should call this to notify the system when explicit health check passes
+ * for {@code packageName};
+ */
+ public final void notifyHealthCheckPassed(@NonNull String packageName) {
+ mHandler.post(() -> {
+ if (mCallback != null) {
+ Objects.requireNonNull(packageName,
+ "Package passing explicit health check must be non-null");
+ Bundle bundle = new Bundle();
+ bundle.putString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE, packageName);
+ mCallback.sendResult(bundle);
+ } else {
+ Log.wtf(TAG, "System missed explicit health check result for " + packageName);
+ }
+ });
+ }
+
+ /**
+ * A PackageConfig contains a package supporting explicit health checks and the
+ * timeout in {@link System#uptimeMillis} across reboots after which health
+ * check requests from clients are failed.
+ *
+ * @hide
+ */
+ @TestApi
+ @SystemApi
+ public static final class PackageConfig implements Parcelable {
+ private static final long DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(1);
+
+ private final String mPackageName;
+ private final long mHealthCheckTimeoutMillis;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param packageName the package name
+ * @param durationMillis the duration in milliseconds, must be greater than or
+ * equal to 0. If it is 0, it will use a system default value.
+ */
+ public PackageConfig(@NonNull String packageName, long healthCheckTimeoutMillis) {
+ mPackageName = Preconditions.checkNotNull(packageName);
+ if (healthCheckTimeoutMillis == 0) {
+ mHealthCheckTimeoutMillis = DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS;
+ } else {
+ mHealthCheckTimeoutMillis = Preconditions.checkArgumentNonnegative(
+ healthCheckTimeoutMillis);
+ }
+ }
+
+ private PackageConfig(Parcel parcel) {
+ mPackageName = parcel.readString();
+ mHealthCheckTimeoutMillis = parcel.readLong();
+ }
+
+ /**
+ * Gets the package name.
+ *
+ * @return the package name
+ */
+ public @NonNull String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Gets the timeout in milliseconds to evaluate an explicit health check result after a
+ * request.
+ *
+ * @return the duration in {@link System#uptimeMillis} across reboots
+ */
+ public long getHealthCheckTimeoutMillis() {
+ return mHealthCheckTimeoutMillis;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "PackageConfig{" + mPackageName + ", " + mHealthCheckTimeoutMillis + "}";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof PackageConfig)) {
+ return false;
+ }
+
+ PackageConfig otherInfo = (PackageConfig) other;
+ return Objects.equals(otherInfo.getHealthCheckTimeoutMillis(),
+ mHealthCheckTimeoutMillis)
+ && Objects.equals(otherInfo.getPackageName(), mPackageName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPackageName, mHealthCheckTimeoutMillis);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@SuppressLint({"MissingNullability"}) Parcel parcel, int flags) {
+ parcel.writeString(mPackageName);
+ parcel.writeLong(mHealthCheckTimeoutMillis);
+ }
+
+ public static final @NonNull Creator<PackageConfig> CREATOR = new Creator<PackageConfig>() {
+ @Override
+ public PackageConfig createFromParcel(Parcel source) {
+ return new PackageConfig(source);
+ }
+
+ @Override
+ public PackageConfig[] newArray(int size) {
+ return new PackageConfig[size];
+ }
+ };
+ }
+
+
+ private class ExplicitHealthCheckServiceWrapper extends IExplicitHealthCheckService.Stub {
+ @Override
+ public void setCallback(RemoteCallback callback) throws RemoteException {
+ mHandler.post(() -> {
+ mCallback = callback;
+ });
+ }
+
+ @Override
+ public void request(String packageName) throws RemoteException {
+ mHandler.post(() -> ExplicitHealthCheckService.this.onRequestHealthCheck(packageName));
+ }
+
+ @Override
+ public void cancel(String packageName) throws RemoteException {
+ mHandler.post(() -> ExplicitHealthCheckService.this.onCancelHealthCheck(packageName));
+ }
+
+ @Override
+ public void getSupportedPackages(RemoteCallback callback) throws RemoteException {
+ mHandler.post(() -> {
+ List<PackageConfig> packages =
+ ExplicitHealthCheckService.this.onGetSupportedPackages();
+ Objects.requireNonNull(packages, "Supported package list must be non-null");
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, new ArrayList<>(packages));
+ callback.sendResult(bundle);
+ });
+ }
+
+ @Override
+ public void getRequestedPackages(RemoteCallback callback) throws RemoteException {
+ mHandler.post(() -> {
+ List<String> packages =
+ ExplicitHealthCheckService.this.onGetRequestedPackages();
+ Objects.requireNonNull(packages, "Requested package list must be non-null");
+ Bundle bundle = new Bundle();
+ bundle.putStringArrayList(EXTRA_REQUESTED_PACKAGES, new ArrayList<>(packages));
+ callback.sendResult(bundle);
+ });
+ }
+ }
+}