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); + }); + } + } +}