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
+ * &mdash;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&mdash;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
+     * &mdash;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&mdash;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
+     * &mdash;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&mdash;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&mdash;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 &mdash; 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
+ * &mdash; for example, because the data was created by a previous version of the app that did not
+ * use the Autofill Framework &mdash; 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&mdash;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> &lt;service android:name=".MyAutofillService"
+ *              android:permission="android.permission.BIND_AUTOFILL_SERVICE"&gt;
+ *     &lt;intent-filter&gt;
+ *         &lt;action android:name="android.service.autofill.AutofillService" /&gt;
+ *     &lt;/intent-filter&gt;
+ *     &lt;meta-data android:name="android.autofill" android:resource="@xml/autofillservice" /&gt;
+ * &lt;/service&gt;</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> &lt;autofill-service xmlns:android="http://schemas.android.com/apk/res/android"&gt;
+ *     &lt;compatibility-package android:name="foo.bar.baz" android:maxLongVersionCode="1000000000"/&gt;
+ * &lt;/autofill-service&gt;</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&mdash;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&mdash;the browser's or the platform's&mdash;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>&lt;{@link
+     * android.R.styleable#AutofillService autofill-service}&gt;</code> tag.
+     * This is a a sample XML file configuring an AutoFillService:
+     * <pre> &lt;autofill-service
+     *     android:settingsActivity="foo.bar.SettingsActivity"
+     *     . . .
+     * /&gt;</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&mdash;for example, because it must
+     * launch an activity asking the user to authenticate first or because the network is
+     * down&mdash;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">
+ * &lt;LinearLayout&gt;
+ *   &lt;ImageView android:id="@+id/templateccLogo"/&gt;
+ *   &lt;TextView android:id="@+id/templateCcNumber"/&gt;
+ *   &lt;TextView android:id="@+id/templateExpDate"/&gt;
+ * &lt;/LinearLayout&gt;
+ * </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&mdash;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&dash; 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}&mdash;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)}&mdash;
+     * 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&mdash;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
+         * &mdash;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
+         * &mdash;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&mdash;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&mdash;for example, launching an
+     * activity asking the user to authenticate first &mdash;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&mdash;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
+         * &mdash;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&mdash;
+         * 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>
+ * &lt;service
+ *    android:name=".MyCarrierMessagingClientService"
+ *    android:exported="false"
+ *    android:process=":persistent"
+ *    android:permission="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE"&gt;
+ *    &lt;intent-filter&gt;
+ *        &lt;action android:name="android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE" /&gt;
+ *    &lt;/intent-filter&gt;
+ * &lt;/service&gt;
+ * </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>
+ * &lt;service android:name=".MyMessagingService"
+ *          android:label="&#64;string/service_name"
+ *          android:permission="android.permission.BIND_CARRIER_SERVICES">
+ *     &lt;intent-filter>
+ *         &lt;action android:name="android.service.carrier.CarrierMessagingService" />
+ *     &lt;/intent-filter>
+ * &lt;/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>
+ *     &lt;service android:name=".MyChooserTargetService"
+ *             android:label="&#64;string/service_name"
+ *             android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
+ *         &lt;intent-filter>
+ *             &lt;action android:name="android.service.chooser.ChooserTargetService" />
+ *         &lt;/intent-filter>
+ *     &lt;/service>
+ * </pre>
+ *
+ * <p>For the system to query your service, you must add a &lt;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>
+ *     &lt;activity android:name=".MyShareActivity"
+ *             android:label="&#64;string/share_activity_label">
+ *         &lt;intent-filter>
+ *             &lt;action android:name="android.intent.action.SEND" />
+ *         &lt;/intent-filter>
+ *         &lt;meta-data android:name="android.service.chooser.chooser_target_service"
+ *                 android:value=".MyChooserTargetService" />
+ *     &lt;/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>&lt;{@link
+     * android.R.styleable#ContentCaptureService content-capture-service}&gt;</code> tag.
+     *
+     * <p>Here's an example of how to use it on {@code AndroidManifest.xml}:
+     *
+     * <pre>
+     * &lt;service android:name=".MyContentCaptureService"
+     *     android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE"&gt;
+     *   &lt;intent-filter&gt;
+     *     &lt;action android:name="android.service.contentcapture.ContentCaptureService" /&gt;
+     *   &lt;/intent-filter&gt;
+     *
+     *   &lt;meta-data
+     *       android:name="android.content_capture"
+     *       android:resource="@xml/my_content_capture_service"/&gt;
+     * &lt;/service&gt;
+     * </pre>
+     *
+     * <p>And then on {@code res/xml/my_content_capture_service.xml}:
+     *
+     * <pre>
+     *   &lt;content-capture-service xmlns:android="http://schemas.android.com/apk/res/android"
+     *       android:settingsActivity="my.package.MySettingsActivity"&gt;
+     *   &lt;/content-capture-service&gt;
+     * </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>
+ * &lt;service
+ *     android:name=".MyDream"
+ *     android:exported="true"
+ *     android:icon="@drawable/my_icon"
+ *     android:label="@string/my_dream_label" >
+ *
+ *     &lt;intent-filter>
+ *         &lt;action android:name="android.service.dreams.DreamService" />
+ *         &lt;category android:name="android.intent.category.DEFAULT" />
+ *     &lt;/intent-filter>
+ *
+ *     &lt;!-- Point to additional information for this dream (optional) -->
+ *     &lt;meta-data
+ *         android:name="android.service.dream"
+ *         android:resource="@xml/my_dream" />
+ * &lt;/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 &lt;dream&gt;} 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>
+ * &lt;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 {
+ *
+ *     &#64;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>
+ * &lt;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">
+ *   &lt;intent-filter>
+ *     &lt;action android:name=”android.service.dreams.DreamService” />
+ *     &lt;category android:name=”android.intent.category.DEFAULT” />
+ *   &lt;/intent-filter>
+ * &lt;/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>&lt;{@link android.R.styleable#Dream dream}&gt;</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>
+ * &lt;service android:name=".MyMediaBrowserService"
+ *          android:label="&#64;string/service_name" >
+ *     &lt;intent-filter>
+ *         &lt;action android:name="android.media.browse.MediaBrowserService" />
+ *     &lt;/intent-filter>
+ * &lt;/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>
+ * &lt;service android:name=".MyConditionProvider"
+ *          android:label="&#64;string/service_name"
+ *          android:permission="android.permission.BIND_CONDITION_PROVIDER_SERVICE">
+ *     &lt;intent-filter>
+ *         &lt;action android:name="android.service.notification.ConditionProviderService" />
+ *     &lt;/intent-filter>
+ *     &lt;meta-data
+ *               android:name="android.service.zen.automatic.ruleType"
+ *               android:value="@string/my_condition_rule">
+ *           &lt;/meta-data>
+ *           &lt;meta-data
+ *               android:name="android.service.zen.automatic.configurationActivity"
+ *               android:value="com.my.package/.MyConditionConfigurationActivity">
+ *           &lt;/meta-data>
+ * &lt;/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>
+ * &lt;service android:name=".NotificationListener"
+ *          android:label="&#64;string/service_name"
+ *          android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+ *     &lt;intent-filter>
+ *         &lt;action android:name="android.service.notification.NotificationListenerService" />
+ *     &lt;/intent-filter>
+ * &lt;/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 &amp; 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 &amp; 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>&lt;{@link
+     * android.R.styleable#QuickAccessWalletService quickaccesswallet-service}&gt;</code> tag. This
+     * is a a sample XML file configuring an QuickAccessWalletService:
+     * <pre> &lt;quickaccesswallet-service
+     *     android:walletActivity="foo.bar.WalletActivity"
+     *     . . .
+     * /&gt;</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>
+ *     &lt;service android:name=".MyResolverRankerService"
+ *             android:exported="true"
+ *             android:priority="100"
+ *             android:permission="android.permission.BIND_RESOLVER_RANKER_SERVICE"&gt;
+ *         &lt;intent-filter&gt;
+ *             &lt;action android:name="android.service.resolver.ResolverRankerService" /&gt;
+ *         &lt;/intent-filter&gt;
+ *     &lt;/service&gt;
+ * </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>
+ *     &lt;service android:name=".ExternalStorageServiceImpl"
+ *             android:exported="true"
+ *             android:priority="100"
+ *             android:permission="android.permission.BIND_EXTERNAL_STORAGE_SERVICE"&gt;
+ *         &lt;intent-filter&gt;
+ *             &lt;action android:name="android.service.storage.ExternalStorageService" /&gt;
+ *         &lt;/intent-filter&gt;
+ *     &lt;/service&gt;
+ * </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>
+ * &lt;service android:name=".TrustAgent"
+ *          android:label="&#64;string/service_name"
+ *          android:permission="android.permission.BIND_TRUST_AGENT">
+ *     &lt;intent-filter>
+ *         &lt;action android:name="android.service.trust.TrustAgentService" />
+ *     &lt;/intent-filter>
+ *     &lt;meta-data android:name="android.service.trust.trustagent"
+ *          android:value="&#64;xml/trust_agent" />
+ * &lt;/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>
+ * &lt;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>&lt;{@link
+     * android.R.styleable#VoiceInteractionService voice-interaction-service}&gt;</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>
+ * &lt;service android:name=".VrListener"
+ *          android:label="&#64;string/service_name"
+ *          android:permission="android.permission.BIND_VR_LISTENER_SERVICE">
+ *     &lt;intent-filter>
+ *         &lt;action android:name="android.service.vr.VrListenerService" />
+ *     &lt;/intent-filter>
+ * &lt;/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>&lt;{@link android.R.styleable#Wallpaper wallpaper}&gt;</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>
+ *     &lt;service android:name=".FooExplicitHealthCheckService"
+ *             android:exported="true"
+ *             android:priority="100"
+ *             android:permission="android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"&gt;
+ *         &lt;intent-filter&gt;
+ *             &lt;action android:name="android.service.watchdog.ExplicitHealthCheckService" /&gt;
+ *         &lt;/intent-filter&gt;
+ *     &lt;/service&gt;
+ * </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);
+            });
+        }
+    }
+}