Merge "Make data RAT icon test pass"
diff --git a/api/system-current.txt b/api/system-current.txt
index 50a8ea4..16404e4 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4332,6 +4332,7 @@
   public final class StatsManager {
     method public boolean addConfiguration(java.lang.String, byte[], java.lang.String, java.lang.String);
     method public byte[] getData(java.lang.String);
+    method public byte[] getMetadata();
     method public boolean removeConfiguration(java.lang.String);
   }
 
diff --git a/api/test-current.txt b/api/test-current.txt
index b181538..de0945b 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -457,19 +457,12 @@
     method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception;
   }
 
-  public final class FieldsDetection implements android.os.Parcelable {
-    ctor public FieldsDetection(android.view.autofill.AutofillId, java.lang.String, java.lang.String);
-    method public int describeContents();
-    method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<android.service.autofill.FieldsDetection> CREATOR;
-  }
-
   public static final class FillEventHistory.Event {
-    method public java.util.Map<java.lang.String, java.lang.Integer> getDetectedFields();
+    method public java.util.Map<java.lang.String, java.lang.Integer> getFieldsClassification();
   }
 
   public static final class FillResponse.Builder {
-    method public android.service.autofill.FillResponse.Builder setFieldsDetection(android.service.autofill.FieldsDetection);
+    method public android.service.autofill.FillResponse.Builder setFieldClassificationIds(android.view.autofill.AutofillId...);
   }
 
   public final class ImageTransformation extends android.service.autofill.InternalTransformation implements android.os.Parcelable android.service.autofill.Transformation {
@@ -501,6 +494,22 @@
     method public android.view.autofill.AutofillValue sanitize(android.view.autofill.AutofillValue);
   }
 
+  public final class UserData implements android.os.Parcelable {
+    method public int describeContents();
+    method public static int getMaxFieldClassificationIdsSize();
+    method public static int getMaxUserDataSize();
+    method public static int getMaxValueLength();
+    method public static int getMinValueLength();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.service.autofill.UserData> CREATOR;
+  }
+
+  public static final class UserData.Builder {
+    ctor public UserData.Builder(java.lang.String, java.lang.String);
+    method public android.service.autofill.UserData.Builder add(java.lang.String, java.lang.String);
+    method public android.service.autofill.UserData build();
+  }
+
   public abstract interface ValueFinder {
     method public abstract java.lang.String findByAutofillId(android.view.autofill.AutofillId);
   }
@@ -983,6 +992,11 @@
     ctor public AutofillId(int);
   }
 
+  public final class AutofillManager {
+    method public android.service.autofill.UserData getUserData();
+    method public void setUserData(android.service.autofill.UserData);
+  }
+
 }
 
 package android.widget {
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index eb3ea0b..0e9cd3b 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -715,6 +715,18 @@
     }
 }
 
+Status StatsService::getMetadata(vector<uint8_t>* output) {
+    IPCThreadState* ipc = IPCThreadState::self();
+    VLOG("StatsService::getMetadata with Pid %i, Uid %i", ipc->getCallingPid(),
+         ipc->getCallingUid());
+    if (checkCallingPermission(String16(kPermissionDump))) {
+        StatsdStats::getInstance().dumpStats(output, false); // Don't reset the counters.
+        return Status::ok();
+    } else {
+        return Status::fromExceptionCode(binder::Status::EX_SECURITY);
+    }
+}
+
 Status StatsService::addConfiguration(const String16& key,
                                       const vector <uint8_t>& config,
                                       const String16& package, const String16& cls,
diff --git a/cmds/statsd/src/StatsService.h b/cmds/statsd/src/StatsService.h
index 007227e..03bc6d9 100644
--- a/cmds/statsd/src/StatsService.h
+++ b/cmds/statsd/src/StatsService.h
@@ -76,6 +76,11 @@
     virtual Status getData(const String16& key, vector<uint8_t>* output) override;
 
     /**
+     * Binder call for clients to get metadata across all configs in statsd.
+     */
+    virtual Status getMetadata(vector<uint8_t>* output) override;
+
+    /**
      * Binder call to let clients send a configuration and indicate they're interested when they
      * should requestData for this configuration.
      */
diff --git a/core/java/android/os/IStatsManager.aidl b/core/java/android/os/IStatsManager.aidl
index b814b46..c8c428e 100644
--- a/core/java/android/os/IStatsManager.aidl
+++ b/core/java/android/os/IStatsManager.aidl
@@ -69,11 +69,16 @@
 
     /**
      * Fetches data for the specified configuration key. Returns a byte array representing proto
-     * wire-encoded of ConfigMetricsReport.
+     * wire-encoded of ConfigMetricsReportList.
      */
     byte[] getData(in String key);
 
     /**
+     * Fetches metadata across statsd. Returns byte array representing wire-encoded proto.
+     */
+    byte[] getMetadata();
+
+    /**
      * Sets a configuration with the specified config key and subscribes to updates for this
      * configuration key. Broadcasts will be sent if this configuration needs to be collected.
      * The configuration must be a wire-encoded StatsDConfig. The caller specifies the name of the
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c4c6798..53832c7 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5333,6 +5333,42 @@
         public static final String AUTOFILL_FEATURE_FIELD_DETECTION = "autofill_field_detection";
 
         /**
+         * Experimental autofill feature.
+         *
+         * <p>TODO(b/67867469): document (or remove) once feature is finished
+         * @hide
+         */
+        public static final String AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE =
+                "autofill_user_data_max_user_data_size";
+
+        /**
+         * Experimental autofill feature.
+         *
+         * <p>TODO(b/67867469): document (or remove) once feature is finished
+         * @hide
+         */
+        public static final String AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE =
+                "autofill_user_data_max_field_classification_size";
+
+        /**
+         * Experimental autofill feature.
+         *
+         * <p>TODO(b/67867469): document (or remove) once feature is finished
+         * @hide
+         */
+        public static final String AUTOFILL_USER_DATA_MAX_VALUE_LENGTH =
+                "autofill_user_data_max_value_length";
+
+        /**
+         * Experimental autofill feature.
+         *
+         * <p>TODO(b/67867469): document (or remove) once feature is finished
+         * @hide
+         */
+        public static final String AUTOFILL_USER_DATA_MIN_VALUE_LENGTH =
+                "autofill_user_data_min_value_length";
+
+        /**
          * @deprecated Use {@link android.provider.Settings.Global#DEVICE_PROVISIONED} instead
          */
         @Deprecated
diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java
index cd362c7..1afa8b3 100644
--- a/core/java/android/service/autofill/AutofillService.java
+++ b/core/java/android/service/autofill/AutofillService.java
@@ -440,7 +440,6 @@
  *  save(username, password);
  * </pre>
  *
- *
  * <a name="Privacy"></a>
  * <h3>Privacy</h3>
  *
@@ -453,6 +452,13 @@
  * <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="FieldsClassification"></a>
+ * <h3>Metrics and fields classification</h3
+ *
+ * <p>TODO(b/67867469): document it or remove this section; in particular, document the relationship
+ * between set/getUserData(), FillResponse.setFieldClassificationIds(), and
+ * FillEventHistory.getFieldsClassification.
  */
 public abstract class AutofillService extends Service {
     private static final String TAG = "AutofillService";
diff --git a/core/java/android/service/autofill/FieldsDetection.java b/core/java/android/service/autofill/FieldsDetection.java
deleted file mode 100644
index 550ecf6..0000000
--- a/core/java/android/service/autofill/FieldsDetection.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * 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 android.annotation.TestApi;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.view.autofill.AutofillId;
-
-/**
- * Class by service to improve autofillable fields detection by tracking the meaning of fields
- * manually edited by the user (when they match values provided by the service).
- *
- * TODO(b/67867469):
- *  - proper javadoc
- *  - unhide / remove testApi
- *  - add FieldsDetection management so service can set it just once and reference it in further
- *    calls to improve performance (and also API to refresh it)
- *  - rename to FieldsDetectionInfo or FieldClassification? (same for CTS tests)
- *  - add FieldsDetectionUnitTest once API is well-defined
- * @hide
- */
-@TestApi
-public final class FieldsDetection implements Parcelable {
-
-    private final AutofillId mFieldId;
-    private final String mRemoteId;
-    private final String mValue;
-
-    /**
-     * Creates a field detection for just one field / value pair.
-     *
-     * @param fieldId autofill id of the field in the screen.
-     * @param remoteId id used by the service to identify the field later.
-     * @param value field value known to the service.
-     *
-     * TODO(b/67867469):
-     *  - proper javadoc
-     *  - change signature to allow more fields / values / match methods
-     *    - might also need to use a builder, where the constructor is the id for the fieldsdetector
-     *    - might need id for values as well
-     *  - add @NonNull / check it / add unit tests
-     *  - make 'value' input more generic so it can accept distance-based match and other matches
-     *  - throw exception if field value is less than X characters (somewhere between 7-10)
-     *  - make sure to limit total number of fields to around 10 or so
-     *  - use AutofillValue instead of String (so it can compare dates, for example)
-     */
-    public FieldsDetection(AutofillId fieldId, String remoteId, String value) {
-        mFieldId = fieldId;
-        mRemoteId = remoteId;
-        mValue = value;
-    }
-
-    /** @hide */
-    public AutofillId getFieldId() {
-        return mFieldId;
-    }
-
-    /** @hide */
-    public String getRemoteId() {
-        return mRemoteId;
-    }
-
-    /** @hide */
-    public String getValue() {
-        return mValue;
-    }
-
-    /////////////////////////////////////
-    // Object "contract" methods. //
-    /////////////////////////////////////
-    @Override
-    public String toString() {
-        // Cannot disclose remoteId or value because they could contain PII
-        return new StringBuilder("FieldsDetection: [field=").append(mFieldId)
-                .append(", remoteId_length=").append(mRemoteId.length())
-                .append(", value_length=").append(mValue.length())
-                .append("]").toString();
-    }
-
-    /////////////////////////////////////
-    // Parcelable "contract" methods. //
-    /////////////////////////////////////
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(Parcel parcel, int flags) {
-        parcel.writeParcelable(mFieldId, flags);
-        parcel.writeString(mRemoteId);
-        parcel.writeString(mValue);
-    }
-
-    public static final Parcelable.Creator<FieldsDetection> CREATOR =
-            new Parcelable.Creator<FieldsDetection>() {
-        @Override
-        public FieldsDetection createFromParcel(Parcel parcel) {
-            // TODO(b/67867469): remove comment below if it does not use a builder at the end
-            // 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.
-            return new FieldsDetection(parcel.readParcelable(null), parcel.readString(),
-                    parcel.readString());
-        }
-
-        @Override
-        public FieldsDetection[] newArray(int size) {
-            return new FieldsDetection[size];
-        }
-    };
-}
diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java
index 736d9ef..eedb972 100644
--- a/core/java/android/service/autofill/FillEventHistory.java
+++ b/core/java/android/service/autofill/FillEventHistory.java
@@ -58,11 +58,6 @@
  */
 public final class FillEventHistory implements Parcelable {
     /**
-     * Not in parcel. The UID of the {@link AutofillService} that created the {@link FillResponse}.
-     */
-    private final int mServiceUid;
-
-    /**
      * Not in parcel. The ID of the autofill session that created the {@link FillResponse}.
      */
     private final int mSessionId;
@@ -70,17 +65,6 @@
     @Nullable private final Bundle mClientState;
     @Nullable List<Event> mEvents;
 
-    /**
-     * Gets the UID of the {@link AutofillService} that created the {@link FillResponse}.
-     *
-     * @return The UID of the {@link AutofillService}
-     *
-     * @hide
-     */
-    public int getServiceUid() {
-        return mServiceUid;
-    }
-
     /** @hide */
     public int getSessionId() {
         return mSessionId;
@@ -123,9 +107,8 @@
     /**
      * @hide
      */
-    public FillEventHistory(int serviceUid, int sessionId, @Nullable Bundle clientState) {
+    public FillEventHistory(int sessionId, @Nullable Bundle clientState) {
         mClientState = clientState;
-        mServiceUid = serviceUid;
         mSessionId = sessionId;
     }
 
@@ -364,16 +347,17 @@
         }
 
         /**
-         * Gets the results of the last {@link FieldsDetection} request.
+         * Gets the results of the last fields classification request.
          *
          * @return map of edit-distance match ({@code 0} means full match,
-         * {@code 1} means 1 character different, etc...) by remote id (as set in the
-         * {@link FieldsDetection} constructor), or {@code null} if none of the user-input values
+         * {@code 1} means 1 character different, etc...) by remote id (as set on
+         * {@link UserData.Builder#add(String, android.view.autofill.AutofillValue)}),
+         * or {@code null} if none of the user-input values
          * matched the requested detection.
          *
          * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the
-         * service requested {@link FillResponse.Builder#setFieldsDetection(FieldsDetection) fields
-         * detection}.
+         * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)
+         * fields detection}.
          *
          * TODO(b/67867469):
          *  - improve javadoc
@@ -382,11 +366,12 @@
          *  - unhide
          *  - unhide / remove testApi
          *  - add @NonNull / check it / add unit tests
+         *  - add link to AutofillService #FieldsClassification anchor
          *
          * @hide
          */
         @TestApi
-        @NonNull public Map<String, Integer> getDetectedFields() {
+        @NonNull public Map<String, Integer> getFieldsClassification() {
             if (mDetectedRemoteId == null || mDetectedFieldScore == -1) {
                 return Collections.emptyMap();
             }
@@ -534,7 +519,7 @@
             new Parcelable.Creator<FillEventHistory>() {
                 @Override
                 public FillEventHistory createFromParcel(Parcel parcel) {
-                    FillEventHistory selection = new FillEventHistory(0, 0, parcel.readBundle());
+                    FillEventHistory selection = new FillEventHistory(0, parcel.readBundle());
 
                     final int numEvents = parcel.readInt();
                     for (int i = 0; i < numEvents; i++) {
diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java
index 84a0974..dff40ff 100644
--- a/core/java/android/service/autofill/FillResponse.java
+++ b/core/java/android/service/autofill/FillResponse.java
@@ -76,7 +76,7 @@
     private final @Nullable AutofillId[] mAuthenticationIds;
     private final @Nullable AutofillId[] mIgnoredIds;
     private final long mDisableDuration;
-    private final @Nullable FieldsDetection mFieldsDetection;
+    private final @Nullable AutofillId[] mFieldClassificationIds;
     private final int mFlags;
     private int mRequestId;
 
@@ -89,7 +89,7 @@
         mAuthenticationIds = builder.mAuthenticationIds;
         mIgnoredIds = builder.mIgnoredIds;
         mDisableDuration = builder.mDisableDuration;
-        mFieldsDetection = builder.mFieldsDetection;
+        mFieldClassificationIds = builder.mFieldClassificationIds;
         mFlags = builder.mFlags;
         mRequestId = INVALID_REQUEST_ID;
     }
@@ -135,8 +135,8 @@
     }
 
     /** @hide */
-    public @Nullable FieldsDetection getFieldsDetection() {
-        return mFieldsDetection;
+    public @Nullable AutofillId[] getFieldClassificationIds() {
+        return mFieldClassificationIds;
     }
 
     /** @hide */
@@ -175,7 +175,7 @@
         private AutofillId[] mAuthenticationIds;
         private AutofillId[] mIgnoredIds;
         private long mDisableDuration;
-        private FieldsDetection mFieldsDetection;
+        private AutofillId[] mFieldClassificationIds;
         private int mFlags;
         private boolean mDestroyed;
 
@@ -329,21 +329,29 @@
         }
 
         /**
+         * Sets which fields are used for <a href="#FieldsClassification">fields classification</a>
+         *
+         * @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}.
+         *
          * TODO(b/67867469):
-         *  - javadoc it
-         *  - javadoc how to check results
-         *  - unhide
+         *  - improve javadoc: explain relationship with UserData and how to check results
          *  - unhide / remove testApi
-         *  - throw exception (and document) if response has datasets or saveinfo
-         *  - throw exception (and document) if id on fieldsDetection is ignored
+         *  - implement multiple ids
          *
          * @hide
          */
         @TestApi
-        public Builder setFieldsDetection(@NonNull FieldsDetection fieldsDetection) {
+        public Builder setFieldClassificationIds(@NonNull AutofillId... ids) {
             throwIfDestroyed();
             throwIfDisableAutofillCalled();
-            mFieldsDetection = Preconditions.checkNotNull(fieldsDetection);
+            Preconditions.checkArrayElementsNotNull(ids, "ids");
+            Preconditions.checkArgumentInRange(ids.length, 1,
+                    UserData.getMaxFieldClassificationIdsSize(), "ids length");
+            mFieldClassificationIds = ids;
             return this;
         }
 
@@ -391,16 +399,17 @@
          * @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)}, or {@link #setClientState(Bundle)}
-         *       was already called.
+         *       {@link #setSaveInfo(SaveInfo)}, {@link #setClientState(Bundle)}, or
+         *       {link #setFieldClassificationIds(AutofillId...)} was already called.
          */
+        // TODO(b/67867469): add @ to {link setFieldClassificationIds} once it's public
         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
-                    || mFieldsDetection != null || mClientState != null) {
+                    || mFieldClassificationIds != null || mClientState != null) {
                 throw new IllegalStateException("disableAutofill() must be the only method called");
             }
 
@@ -417,15 +426,18 @@
          *   <li>No call was made to {@link #addDataset(Dataset)},
          *       {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)},
          *       {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)},
-         *       or {@link #setClientState(Bundle)}.
+         *       {@link #setClientState(Bundle)},
+         *       or {link #setFieldClassificationIds(AutofillId...)}.
          * </ol>
          *
          * @return A built response.
          */
+        // TODO(b/67867469): add @ to {link setFieldClassificationIds} once it's public
         public FillResponse build() {
             throwIfDestroyed();
             if (mAuthentication == null && mDatasets == null && mSaveInfo == null
-                    && mDisableDuration == 0 && mFieldsDetection == null && mClientState == 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");
@@ -466,7 +478,8 @@
                 .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds))
                 .append(", disableDuration=").append(mDisableDuration)
                 .append(", flags=").append(mFlags)
-                .append(", fieldDetection=").append(mFieldsDetection)
+                .append(", fieldClassificationIds=")
+                    .append(Arrays.toString(mFieldClassificationIds))
                 .append("]")
                 .toString();
     }
@@ -490,7 +503,7 @@
         parcel.writeParcelable(mPresentation, flags);
         parcel.writeParcelableArray(mIgnoredIds, flags);
         parcel.writeLong(mDisableDuration);
-        parcel.writeParcelable(mFieldsDetection, flags);
+        parcel.writeParcelableArray(mFieldClassificationIds, flags);
         parcel.writeInt(mFlags);
         parcel.writeInt(mRequestId);
     }
@@ -526,9 +539,10 @@
             if (disableDuration > 0) {
                 builder.disableAutofill(disableDuration);
             }
-            final FieldsDetection fieldsDetection = parcel.readParcelable(null);
-            if (fieldsDetection != null) {
-                builder.setFieldsDetection(fieldsDetection);
+            final AutofillId[] fieldClassifactionIds =
+                    parcel.readParcelableArray(null, AutofillId.class);
+            if (fieldClassifactionIds != null) {
+                builder.setFieldClassificationIds(fieldClassifactionIds);
             }
             builder.setFlags(parcel.readInt());
 
diff --git a/core/java/android/service/autofill/UserData.aidl b/core/java/android/service/autofill/UserData.aidl
new file mode 100644
index 0000000..76016de
--- /dev/null
+++ b/core/java/android/service/autofill/UserData.aidl
@@ -0,0 +1,20 @@
+/**
+ * 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;
+
+parcelable UserData;
+parcelable UserData.Constraints;
diff --git a/core/java/android/service/autofill/UserData.java b/core/java/android/service/autofill/UserData.java
new file mode 100644
index 0000000..16d8d4a
--- /dev/null
+++ b/core/java/android/service/autofill/UserData.java
@@ -0,0 +1,288 @@
+/*
+ * 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_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.Parcel;
+import android.os.Parcelable;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.autofill.Helper;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Class used by service to improve autofillable fields detection by tracking the meaning of fields
+ * manually edited by the user (when they match values provided by the service).
+ *
+ * TODO(b/67867469):
+ *  - improve javadoc / add link to section on AutofillService
+ *  - unhide / remove testApi
+ * @hide
+ */
+@TestApi
+public final class UserData implements Parcelable {
+
+    private static final String TAG = "UserData";
+
+    private static final int DEFAULT_MAX_USER_DATA_SIZE = 10;
+    private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10;
+    private static final int DEFAULT_MIN_VALUE_LENGTH = 5;
+    private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
+
+    private final String[] mRemoteIds;
+    private final String[] mValues;
+
+    private UserData(Builder builder) {
+        mRemoteIds = new String[builder.mRemoteIds.size()];
+        builder.mRemoteIds.toArray(mRemoteIds);
+        mValues = new String[builder.mValues.size()];
+        builder.mValues.toArray(mValues);
+    }
+
+    /** @hide */
+    public String[] getRemoteIds() {
+        return mRemoteIds;
+    }
+
+    /** @hide */
+    public String[] getValues() {
+        return mValues;
+    }
+
+    /** @hide */
+    public void dump(String prefix, PrintWriter pw) {
+        // Cannot disclose remote ids because they could contain PII
+        pw.print(prefix); pw.print("Remote ids size: "); pw.println(mRemoteIds.length);
+        for (int i = 0; i < mValues.length; i++) {
+            pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); pw.println(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("minValueLength: "); pw.println(getMinValueLength());
+        pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength());
+    }
+
+    /**
+     * A builder for {@link UserData} objects.
+     *
+     * TODO(b/67867469): unhide / remove testApi
+     *
+     * @hide
+     */
+    @TestApi
+    public static final class Builder {
+        private final ArraySet<String> mRemoteIds;
+        private final ArrayList<String> mValues;
+        private boolean mDestroyed;
+
+        /**
+         * Creates a new builder for the user data used for <a href="#FieldsClassification">fields
+         * classification</a>.
+         *
+         * @throws IllegalArgumentException if {@code remoteId} or {@code value} are empty or if the
+         * length of {@code value} is lower than {@link UserData#getMinValueLength()}
+         * or higher than {@link UserData#getMaxValueLength()}.
+         */
+        public Builder(@NonNull String remoteId, @NonNull String value) {
+            checkValidRemoteId(remoteId);
+            checkValidValue(value);
+            final int capacity = getMaxUserDataSize();
+            mRemoteIds = new ArraySet<>(capacity);
+            mValues = new ArrayList<>(capacity);
+            mRemoteIds.add(remoteId);
+            mValues.add(value);
+        }
+
+        /**
+         * Adds a new value for user data.
+         *
+         * @param remoteId unique string used to identify the user data.
+         * @param value value of the user data.
+         *
+         * @throws IllegalStateException if {@link #build()} or
+         * {@link #add(String, String)} with the same {@code remoteId} has already
+         * been called, or if the number of values add (i.e., calls made to this method plus
+         * constructor) is more than {@link UserData#getMaxUserDataSize()}.
+         *
+         * @throws IllegalArgumentException if {@code remoteId} or {@code value} are empty or if the
+         * length of {@code value} is lower than {@link UserData#getMinValueLength()}
+         * or higher than {@link UserData#getMaxValueLength()}.
+         */
+        public Builder add(@NonNull String remoteId, @NonNull String value) {
+            throwIfDestroyed();
+            checkValidRemoteId(remoteId);
+            checkValidValue(value);
+
+            Preconditions.checkState(!mRemoteIds.contains(remoteId),
+                    // Don't include remoteId on message because it could contain PII
+                    "already has entry with same remoteId");
+            Preconditions.checkState(mRemoteIds.size() < getMaxUserDataSize(),
+                    "already added " + mRemoteIds.size() + " elements");
+            mRemoteIds.add(remoteId);
+            mValues.add(value);
+            return this;
+        }
+
+        private void checkValidRemoteId(@Nullable String remoteId) {
+            Preconditions.checkNotNull(remoteId);
+            Preconditions.checkArgument(!remoteId.isEmpty(), "remoteId cannot be empty");
+        }
+
+        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.
+         */
+        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();
+
+        // Cannot disclose keys or values because they could contain PII
+        final StringBuilder builder = new StringBuilder("UserData: [remoteIds=");
+        Helper.appendRedacted(builder, mRemoteIds);
+        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.writeStringArray(mRemoteIds);
+        parcel.writeStringArray(mValues);
+    }
+
+    public static final 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[] remoteIds = parcel.readStringArray();
+            final String[] values = parcel.readStringArray();
+            final Builder builder = new Builder(remoteIds[0], values[0]);
+            for (int i = 1; i < remoteIds.length; i++) {
+                builder.add(remoteIds[i], values[i]);
+            }
+            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 minimum length of values passed to {@link Builder#Builder(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 {@link Builder#Builder(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/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 735b822..1ec2406 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -890,7 +890,17 @@
     }
 
     public static boolean isValidScheduleConditionId(Uri conditionId) {
-        return tryParseScheduleConditionId(conditionId) != null;
+        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;
     }
 
     public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) {
diff --git a/core/java/android/text/AutoGrowArray.java b/core/java/android/text/AutoGrowArray.java
deleted file mode 100644
index e428377..0000000
--- a/core/java/android/text/AutoGrowArray.java
+++ /dev/null
@@ -1,374 +0,0 @@
-/*
- * 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.text;
-
-import android.annotation.IntRange;
-import android.annotation.NonNull;
-
-import com.android.internal.util.ArrayUtils;
-
-import libcore.util.EmptyArray;
-
-/**
- * Implements a growing array of int primitives.
- *
- * These arrays are NOT thread safe.
- *
- * @hide
- */
-public final class AutoGrowArray {
-    private static final int MIN_CAPACITY_INCREMENT = 12;
-    private static final int MAX_CAPACITY_TO_BE_KEPT = 10000;
-
-    /**
-     * Returns next capacity size.
-     *
-     * The returned capacity is larger than requested capacity.
-     */
-    private static int computeNewCapacity(int currentSize, int requested) {
-        final int targetCapacity = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2)
-                ?  MIN_CAPACITY_INCREMENT : currentSize >> 1);
-        return targetCapacity > requested ? targetCapacity : requested;
-    }
-
-    /**
-     * An auto growing byte array.
-     */
-    public static class ByteArray {
-
-        private @NonNull byte[] mValues;
-        private @IntRange(from = 0) int mSize;
-
-        /**
-         * Creates an empty ByteArray with the default initial capacity.
-         */
-        public ByteArray() {
-            this(10);
-        }
-
-        /**
-         * Creates an empty ByteArray with the specified initial capacity.
-         */
-        public ByteArray(@IntRange(from = 0) int initialCapacity) {
-            if (initialCapacity == 0) {
-                mValues = EmptyArray.BYTE;
-            } else {
-                mValues = ArrayUtils.newUnpaddedByteArray(initialCapacity);
-            }
-            mSize = 0;
-        }
-
-        /**
-         * Changes the size of this ByteArray. If this ByteArray is shrinked, the backing array
-         * capacity is unchanged.
-         */
-        public void resize(@IntRange(from = 0) int newSize) {
-            if (newSize > mValues.length) {
-                ensureCapacity(newSize - mSize);
-            }
-            mSize = newSize;
-        }
-
-        /**
-         * Appends the specified value to the end of this array.
-         */
-        public void append(byte value) {
-            ensureCapacity(1);
-            mValues[mSize++] = value;
-        }
-
-        /**
-         * Ensures capacity to append at least <code>count</code> values.
-         */
-        private void ensureCapacity(@IntRange int count) {
-            final int requestedSize = mSize + count;
-            if (requestedSize >= mValues.length) {
-                final int newCapacity = computeNewCapacity(mSize, requestedSize);
-                final byte[] newValues = ArrayUtils.newUnpaddedByteArray(newCapacity);
-                System.arraycopy(mValues, 0, newValues, 0, mSize);
-                mValues = newValues;
-            }
-        }
-
-        /**
-         * Removes all values from this array.
-         */
-        public void clear() {
-            mSize = 0;
-        }
-
-        /**
-         * Removes all values from this array and release the internal array object if it is too
-         * large.
-         */
-        public void clearWithReleasingLargeArray() {
-            clear();
-            if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
-                mValues = EmptyArray.BYTE;
-            }
-        }
-
-        /**
-         * Returns the value at the specified position in this array.
-         */
-        public byte get(@IntRange(from = 0) int index) {
-            return mValues[index];
-        }
-
-        /**
-         * Sets the value at the specified position in this array.
-         */
-        public void set(@IntRange(from = 0) int index, byte value) {
-            mValues[index] = value;
-        }
-
-        /**
-         * Returns the number of values in this array.
-         */
-        public @IntRange(from = 0) int size() {
-            return mSize;
-        }
-
-        /**
-         * Returns internal raw array.
-         *
-         * Note that this array may have larger size than you requested.
-         * Use size() instead for getting the actual array size.
-         */
-        public @NonNull byte[] getRawArray() {
-            return mValues;
-        }
-    }
-
-    /**
-     * An auto growing int array.
-     */
-    public static class IntArray {
-
-        private @NonNull int[] mValues;
-        private @IntRange(from = 0) int mSize;
-
-        /**
-         * Creates an empty IntArray with the default initial capacity.
-         */
-        public IntArray() {
-            this(10);
-        }
-
-        /**
-         * Creates an empty IntArray with the specified initial capacity.
-         */
-        public IntArray(@IntRange(from = 0) int initialCapacity) {
-            if (initialCapacity == 0) {
-                mValues = EmptyArray.INT;
-            } else {
-                mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity);
-            }
-            mSize = 0;
-        }
-
-        /**
-         * Changes the size of this IntArray. If this IntArray is shrinked, the backing array
-         * capacity is unchanged.
-         */
-        public void resize(@IntRange(from = 0) int newSize) {
-            if (newSize > mValues.length) {
-                ensureCapacity(newSize - mSize);
-            }
-            mSize = newSize;
-        }
-
-        /**
-         * Appends the specified value to the end of this array.
-         */
-        public void append(int value) {
-            ensureCapacity(1);
-            mValues[mSize++] = value;
-        }
-
-        /**
-         * Ensures capacity to append at least <code>count</code> values.
-         */
-        private void ensureCapacity(@IntRange(from = 0) int count) {
-            final int requestedSize = mSize + count;
-            if (requestedSize >= mValues.length) {
-                final int newCapacity = computeNewCapacity(mSize, requestedSize);
-                final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity);
-                System.arraycopy(mValues, 0, newValues, 0, mSize);
-                mValues = newValues;
-            }
-        }
-
-        /**
-         * Removes all values from this array.
-         */
-        public void clear() {
-            mSize = 0;
-        }
-
-        /**
-         * Removes all values from this array and release the internal array object if it is too
-         * large.
-         */
-        public void clearWithReleasingLargeArray() {
-            clear();
-            if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
-                mValues = EmptyArray.INT;
-            }
-        }
-
-        /**
-         * Returns the value at the specified position in this array.
-         */
-        public int get(@IntRange(from = 0) int index) {
-            return mValues[index];
-        }
-
-        /**
-         * Sets the value at the specified position in this array.
-         */
-        public void set(@IntRange(from = 0) int index, int value) {
-            mValues[index] = value;
-        }
-
-        /**
-         * Returns the number of values in this array.
-         */
-        public @IntRange(from = 0) int size() {
-            return mSize;
-        }
-
-        /**
-         * Returns internal raw array.
-         *
-         * Note that this array may have larger size than you requested.
-         * Use size() instead for getting the actual array size.
-         */
-        public @NonNull int[] getRawArray() {
-            return mValues;
-        }
-    }
-
-    /**
-     * An auto growing float array.
-     */
-    public static class FloatArray {
-
-        private @NonNull float[] mValues;
-        private @IntRange(from = 0) int mSize;
-
-        /**
-         * Creates an empty FloatArray with the default initial capacity.
-         */
-        public FloatArray() {
-            this(10);
-        }
-
-        /**
-         * Creates an empty FloatArray with the specified initial capacity.
-         */
-        public FloatArray(@IntRange(from = 0) int initialCapacity) {
-            if (initialCapacity == 0) {
-                mValues = EmptyArray.FLOAT;
-            } else {
-                mValues = ArrayUtils.newUnpaddedFloatArray(initialCapacity);
-            }
-            mSize = 0;
-        }
-
-        /**
-         * Changes the size of this FloatArray. If this FloatArray is shrinked, the backing array
-         * capacity is unchanged.
-         */
-        public void resize(@IntRange(from = 0) int newSize) {
-            if (newSize > mValues.length) {
-                ensureCapacity(newSize - mSize);
-            }
-            mSize = newSize;
-        }
-
-        /**
-         * Appends the specified value to the end of this array.
-         */
-        public void append(float value) {
-            ensureCapacity(1);
-            mValues[mSize++] = value;
-        }
-
-        /**
-         * Ensures capacity to append at least <code>count</code> values.
-         */
-        private void ensureCapacity(int count) {
-            final int requestedSize = mSize + count;
-            if (requestedSize >= mValues.length) {
-                final int newCapacity = computeNewCapacity(mSize, requestedSize);
-                final float[] newValues = ArrayUtils.newUnpaddedFloatArray(newCapacity);
-                System.arraycopy(mValues, 0, newValues, 0, mSize);
-                mValues = newValues;
-            }
-        }
-
-        /**
-         * Removes all values from this array.
-         */
-        public void clear() {
-            mSize = 0;
-        }
-
-        /**
-         * Removes all values from this array and release the internal array object if it is too
-         * large.
-         */
-        public void clearWithReleasingLargeArray() {
-            clear();
-            if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
-                mValues = EmptyArray.FLOAT;
-            }
-        }
-
-        /**
-         * Returns the value at the specified position in this array.
-         */
-        public float get(@IntRange(from = 0) int index) {
-            return mValues[index];
-        }
-
-        /**
-         * Sets the value at the specified position in this array.
-         */
-        public void set(@IntRange(from = 0) int index, float value) {
-            mValues[index] = value;
-        }
-
-        /**
-         * Returns the number of values in this array.
-         */
-        public @IntRange(from = 0) int size() {
-            return mSize;
-        }
-
-        /**
-         * Returns internal raw array.
-         *
-         * Note that this array may have larger size than you requested.
-         * Use size() instead for getting the actual array size.
-         */
-        public @NonNull float[] getRawArray() {
-            return mValues;
-        }
-    }
-}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 2a693a1..4d2a962 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -1907,14 +1907,22 @@
 
     private static float measurePara(TextPaint paint, CharSequence text, int start, int end,
             TextDirectionHeuristic textDir) {
-        MeasuredText mt = null;
+        MeasuredText mt = MeasuredText.obtain();
         TextLine tl = TextLine.obtain();
         try {
-            mt = MeasuredText.buildForBidi(text, start, end, textDir, mt);
-            final char[] chars = mt.getChars();
-            final int len = chars.length;
-            final Directions directions = mt.getDirections(0, len);
-            final int dir = mt.getParagraphDir();
+            mt.setPara(text, start, end, textDir);
+            Directions directions;
+            int dir;
+            if (mt.mEasy) {
+                directions = DIRS_ALL_LEFT_TO_RIGHT;
+                dir = Layout.DIR_LEFT_TO_RIGHT;
+            } else {
+                directions = AndroidBidi.directions(mt.mDir, mt.mLevels,
+                    0, mt.mChars, 0, mt.mLen);
+                dir = mt.mDir;
+            }
+            char[] chars = mt.mChars;
+            int len = mt.mLen;
             boolean hasTabs = false;
             TabStops tabStops = null;
             // leading margins should be taken into account when measuring a paragraph
@@ -1947,9 +1955,7 @@
             return margin + Math.abs(tl.metrics(null));
         } finally {
             TextLine.recycle(tl);
-            if (mt != null) {
-                mt.recycle();
-            }
+            MeasuredText.recycle(mt);
         }
     }
 
@@ -2266,11 +2272,6 @@
     private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
     private int mJustificationMode;
 
-    /** @hide */
-    @IntDef({DIR_LEFT_TO_RIGHT, DIR_RIGHT_TO_LEFT})
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface Direction {}
-
     public static final int DIR_LEFT_TO_RIGHT = 1;
     public static final int DIR_RIGHT_TO_LEFT = -1;
 
diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java
index ca31176..3d9fba7 100644
--- a/core/java/android/text/MeasuredText.java
+++ b/core/java/android/text/MeasuredText.java
@@ -16,384 +16,125 @@
 
 package android.text;
 
-import android.annotation.FloatRange;
-import android.annotation.IntRange;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.graphics.Paint;
-import android.text.AutoGrowArray.ByteArray;
-import android.text.AutoGrowArray.FloatArray;
-import android.text.AutoGrowArray.IntArray;
-import android.text.Layout.Directions;
 import android.text.style.MetricAffectingSpan;
 import android.text.style.ReplacementSpan;
-import android.util.Pools.SynchronizedPool;
+import android.util.Log;
 
-import java.util.Arrays;
+import com.android.internal.util.ArrayUtils;
 
 /**
- * MeasuredText provides text information for rendering purpose.
- *
- * The first motivation of this class is identify the text directions and retrieving individual
- * character widths. However retrieving character widths is slower than identifying text directions.
- * Thus, this class provides several builder methods for specific purposes.
- *
- * - buildForBidi:
- *   Compute only text directions.
- * - buildForMeasurement:
- *   Compute text direction and all character widths.
- * - buildForStaticLayout:
- *   This is bit special. StaticLayout also needs to know text direction and character widths for
- *   line breaking, but all things are done in native code. Similarly, text measurement is done
- *   in native code. So instead of storing result to Java array, this keeps the result in native
- *   code since there is no good reason to move the results to Java layer.
- *
- * In addition to the character widths, some additional information is computed for each purposes,
- * e.g. whole text length for measurement or font metrics for static layout.
- *
- * MeasuredText is NOT a thread safe object.
  * @hide
  */
 class MeasuredText {
-    private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
+    private static final boolean localLOGV = false;
+    CharSequence mText;
+    int mTextStart;
+    float[] mWidths;
+    char[] mChars;
+    byte[] mLevels;
+    int mDir;
+    boolean mEasy;
+    int mLen;
 
-    private MeasuredText() {}  // Use build static functions instead.
+    private int mPos;
+    private TextPaint mWorkPaint;
 
-    private static final SynchronizedPool<MeasuredText> sPool = new SynchronizedPool<>(1);
-
-    private static @NonNull MeasuredText obtain() { // Use build static functions instead.
-        final MeasuredText mt = sPool.acquire();
-        return mt != null ? mt : new MeasuredText();
+    private MeasuredText() {
+        mWorkPaint = new TextPaint();
     }
 
-    /**
-     * Recycle the MeasuredText.
-     *
-     * Do not call any methods after you call this method.
-     */
-    public void recycle() {
-        release();
-        sPool.release(this);
-    }
+    private static final Object[] sLock = new Object[0];
+    private static final MeasuredText[] sCached = new MeasuredText[3];
 
-    // The casted original text.
-    //
-    // This may be null if the passed text is not a Spanned.
-    private @Nullable Spanned mSpanned;
-
-    // The start offset of the target range in the original text (mSpanned);
-    private @IntRange(from = 0) int mTextStart;
-
-    // The length of the target range in the original text.
-    private @IntRange(from = 0) int mTextLength;
-
-    // The copied character buffer for measuring text.
-    //
-    // The length of this array is mTextLength.
-    private @Nullable char[] mCopiedBuffer;
-
-    // The whole paragraph direction.
-    private @Layout.Direction int mParaDir;
-
-    // True if the text is LTR direction and doesn't contain any bidi characters.
-    private boolean mLtrWithoutBidi;
-
-    // The bidi level for individual characters.
-    //
-    // This is empty if mLtrWithoutBidi is true.
-    private @NonNull ByteArray mLevels = new ByteArray();
-
-    // The whole width of the text.
-    // See getWholeWidth comments.
-    private @FloatRange(from = 0.0f) float mWholeWidth;
-
-    // Individual characters' widths.
-    // See getWidths comments.
-    private @Nullable FloatArray mWidths = new FloatArray();
-
-    // The span end positions.
-    // See getSpanEndCache comments.
-    private @Nullable IntArray mSpanEndCache = new IntArray(4);
-
-    // The font metrics.
-    // See getFontMetrics comments.
-    private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
-
-    // Following two objects are for avoiding object allocation.
-    private @NonNull TextPaint mCachedPaint = new TextPaint();
-    private @Nullable Paint.FontMetricsInt mCachedFm;
-
-    /**
-     * Releases internal buffers.
-     */
-    public void release() {
-        reset();
-        mLevels.clearWithReleasingLargeArray();
-        mWidths.clearWithReleasingLargeArray();
-        mFontMetrics.clearWithReleasingLargeArray();
-        mSpanEndCache.clearWithReleasingLargeArray();
-    }
-
-    /**
-     * Resets the internal state for starting new text.
-     */
-    private void reset() {
-        mSpanned = null;
-        mCopiedBuffer = null;
-        mWholeWidth = 0;
-        mLevels.clear();
-        mWidths.clear();
-        mFontMetrics.clear();
-        mSpanEndCache.clear();
-    }
-
-    /**
-     * Returns the characters to be measured.
-     *
-     * This is always available.
-     */
-    public @NonNull char[] getChars() {
-        return mCopiedBuffer;
-    }
-
-    /**
-     * Returns the paragraph direction.
-     *
-     * This is always available.
-     */
-    public @Layout.Direction int getParagraphDir() {
-        return mParaDir;
-    }
-
-    /**
-     * Returns the directions.
-     *
-     * This is always available.
-     */
-    public Directions getDirections(@IntRange(from = 0) int start,  // inclusive
-                                    @IntRange(from = 0) int end) {  // exclusive
-        if (mLtrWithoutBidi) {
-            return Layout.DIRS_ALL_LEFT_TO_RIGHT;
-        }
-
-        final int length = end - start;
-        return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
-                length);
-    }
-
-    /**
-     * Returns the whole text width.
-     *
-     * This is available only if the MeasureText is computed with computeForMeasurement.
-     * Returns 0 in other cases.
-     */
-    public @FloatRange(from = 0.0f) float getWholeWidth() {
-        return mWholeWidth;
-    }
-
-    /**
-     * Returns the individual character's width.
-     *
-     * This is available only if the MeasureText is computed with computeForMeasurement.
-     * Returns empty array in other cases.
-     */
-    public @NonNull FloatArray getWidths() {
-        return mWidths;
-    }
-
-    /**
-     * Returns the MetricsAffectingSpan end indices.
-     *
-     * If the input text is not a spanned string, this has one value that is the length of the text.
-     *
-     * This is available only if the MeasureText is computed with computeForStaticLayout.
-     * Returns empty array in other cases.
-     */
-    public @NonNull IntArray getSpanEndCache() {
-        return mSpanEndCache;
-    }
-
-    /**
-     * Returns the int array which holds FontMetrics.
-     *
-     * This array holds the repeat of top, bottom, ascent, descent of font metrics value.
-     *
-     * This is available only if the MeasureText is computed with computeForStaticLayout.
-     * Returns empty array in other cases.
-     */
-    public @NonNull IntArray getFontMetrics() {
-        return mFontMetrics;
-    }
-
-    /**
-     * Generates new MeasuredText for Bidi computation.
-     *
-     * If recycle is null, this returns new instance. If recycle is not null, this fills computed
-     * result to recycle and returns recycle.
-     *
-     * @param text the character sequence to be measured
-     * @param start the inclusive start offset of the target region in the text
-     * @param end the exclusive end offset of the target region in the text
-     * @param textDir the text direction
-     * @param recycle pass existing MeasuredText if you want to recycle it.
-     *
-     * @return measured text
-     */
-    public static @NonNull MeasuredText buildForBidi(@NonNull CharSequence text,
-                                                     @IntRange(from = 0) int start,
-                                                     @IntRange(from = 0) int end,
-                                                     @NonNull TextDirectionHeuristic textDir,
-                                                     @Nullable MeasuredText recycle) {
-        final MeasuredText mt = recycle == null ? obtain() : recycle;
-        mt.resetAndAnalyzeBidi(text, start, end, textDir);
-        return mt;
-    }
-
-    /**
-     * Generates new MeasuredText for measuring texts.
-     *
-     * If recycle is null, this returns new instance. If recycle is not null, this fills computed
-     * result to recycle and returns recycle.
-     *
-     * @param paint the paint to be used for rendering the text.
-     * @param text the character sequence to be measured
-     * @param start the inclusive start offset of the target region in the text
-     * @param end the exclusive end offset of the target region in the text
-     * @param textDir the text direction
-     * @param recycle pass existing MeasuredText if you want to recycle it.
-     *
-     * @return measured text
-     */
-    public static @NonNull MeasuredText buildForMeasurement(@NonNull TextPaint paint,
-                                                            @NonNull CharSequence text,
-                                                            @IntRange(from = 0) int start,
-                                                            @IntRange(from = 0) int end,
-                                                            @NonNull TextDirectionHeuristic textDir,
-                                                            @Nullable MeasuredText recycle) {
-        final MeasuredText mt = recycle == null ? obtain() : recycle;
-        mt.resetAndAnalyzeBidi(text, start, end, textDir);
-
-        mt.mWidths.resize(mt.mTextLength);
-        if (mt.mTextLength == 0) {
-            return mt;
-        }
-
-        if (mt.mSpanned == null) {
-            // No style change by MetricsAffectingSpan. Just measure all text.
-            mt.applyMetricsAffectingSpan(
-                    paint, null /* spans */, start, end, 0 /* native static layout ptr */);
-        } else {
-            // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
-            int spanEnd;
-            for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
-                spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
-                MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
-                        MetricAffectingSpan.class);
-                spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
-                mt.applyMetricsAffectingSpan(
-                        paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */);
+    static MeasuredText obtain() {
+        MeasuredText mt;
+        synchronized (sLock) {
+            for (int i = sCached.length; --i >= 0;) {
+                if (sCached[i] != null) {
+                    mt = sCached[i];
+                    sCached[i] = null;
+                    return mt;
+                }
             }
         }
+        mt = new MeasuredText();
+        if (localLOGV) {
+            Log.v("MEAS", "new: " + mt);
+        }
         return mt;
     }
 
-    /**
-     * Generates new MeasuredText for StaticLayout.
-     *
-     * If recycle is null, this returns new instance. If recycle is not null, this fills computed
-     * result to recycle and returns recycle.
-     *
-     * @param paint the paint to be used for rendering the text.
-     * @param text the character sequence to be measured
-     * @param start the inclusive start offset of the target region in the text
-     * @param end the exclusive end offset of the target region in the text
-     * @param textDir the text direction
-     * @param nativeStaticLayoutPtr the pointer to the native static layout object
-     * @param recycle pass existing MeasuredText if you want to recycle it.
-     *
-     * @return measured text
-     */
-    public static @NonNull MeasuredText buildForStaticLayout(
-            @NonNull TextPaint paint,
-            @NonNull CharSequence text,
-            @IntRange(from = 0) int start,
-            @IntRange(from = 0) int end,
-            @NonNull TextDirectionHeuristic textDir,
-            /* Non-Zero */ long nativeStaticLayoutPtr,
-            @Nullable MeasuredText recycle) {
-        final MeasuredText mt = recycle == null ? obtain() : recycle;
-        mt.resetAndAnalyzeBidi(text, start, end, textDir);
-        if (mt.mTextLength == 0) {
-            return mt;
-        }
-
-        if (mt.mSpanned == null) {
-            // No style change by MetricsAffectingSpan. Just measure all text.
-            mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end,
-                    nativeStaticLayoutPtr);
-            mt.mSpanEndCache.append(end);
-        } else {
-            // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
-            int spanEnd;
-            for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
-                spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
-                MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
-                        MetricAffectingSpan.class);
-                spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
-                mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
-                        nativeStaticLayoutPtr);
-                mt.mSpanEndCache.append(spanEnd);
+    static MeasuredText recycle(MeasuredText mt) {
+        mt.finish();
+        synchronized(sLock) {
+            for (int i = 0; i < sCached.length; ++i) {
+                if (sCached[i] == null) {
+                    sCached[i] = mt;
+                    mt.mText = null;
+                    break;
+                }
             }
         }
+        return null;
+    }
 
-        return mt;
+    void finish() {
+        mText = null;
+        if (mLen > 1000) {
+            mWidths = null;
+            mChars = null;
+            mLevels = null;
+        }
     }
 
     /**
-     * Reset internal state and analyzes text for bidirectional runs.
-     *
-     * @param text the character sequence to be measured
-     * @param start the inclusive start offset of the target region in the text
-     * @param end the exclusive end offset of the target region in the text
-     * @param textDir the text direction
+     * Analyzes text for bidirectional runs.  Allocates working buffers.
      */
-    private void resetAndAnalyzeBidi(@NonNull CharSequence text,
-                                     @IntRange(from = 0) int start,  // inclusive
-                                     @IntRange(from = 0) int end,  // exclusive
-                                     @NonNull TextDirectionHeuristic textDir) {
-        reset();
-        mSpanned = text instanceof Spanned ? (Spanned) text : null;
+    void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
+        mText = text;
         mTextStart = start;
-        mTextLength = end - start;
 
-        if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
-            mCopiedBuffer = new char[mTextLength];
+        int len = end - start;
+        mLen = len;
+        mPos = 0;
+
+        if (mWidths == null || mWidths.length < len) {
+            mWidths = ArrayUtils.newUnpaddedFloatArray(len);
         }
-        TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
+        if (mChars == null || mChars.length != len) {
+            mChars = new char[len];
+        }
+        TextUtils.getChars(text, start, end, mChars, 0);
 
-        // Replace characters associated with ReplacementSpan to U+FFFC.
-        if (mSpanned != null) {
-            ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
+        if (text instanceof Spanned) {
+            Spanned spanned = (Spanned) text;
+            ReplacementSpan[] spans = spanned.getSpans(start, end,
+                    ReplacementSpan.class);
 
             for (int i = 0; i < spans.length; i++) {
-                int startInPara = mSpanned.getSpanStart(spans[i]) - start;
-                int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
-                // The span interval may be larger and must be restricted to [start, end)
+                int startInPara = spanned.getSpanStart(spans[i]) - start;
+                int endInPara = spanned.getSpanEnd(spans[i]) - start;
+                // The span interval may be larger and must be restricted to [start, end[
                 if (startInPara < 0) startInPara = 0;
-                if (endInPara > mTextLength) endInPara = mTextLength;
-                Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
+                if (endInPara > len) endInPara = len;
+                for (int j = startInPara; j < endInPara; j++) {
+                    mChars[j] = '\uFFFC'; // object replacement character
+                }
             }
         }
 
         if ((textDir == TextDirectionHeuristics.LTR ||
                 textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR ||
                 textDir == TextDirectionHeuristics.ANYRTL_LTR) &&
-                TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
-            mLevels.clear();
-            mParaDir = Layout.DIR_LEFT_TO_RIGHT;
-            mLtrWithoutBidi = true;
+                TextUtils.doesNotNeedBidi(mChars, 0, len)) {
+            mDir = Layout.DIR_LEFT_TO_RIGHT;
+            mEasy = true;
         } else {
-            final int bidiRequest;
+            if (mLevels == null || mLevels.length < len) {
+                mLevels = ArrayUtils.newUnpaddedByteArray(len);
+            }
+            int bidiRequest;
             if (textDir == TextDirectionHeuristics.LTR) {
                 bidiRequest = Layout.DIR_REQUEST_LTR;
             } else if (textDir == TextDirectionHeuristics.RTL) {
@@ -403,146 +144,122 @@
             } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
                 bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
             } else {
-                final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
+                boolean isRtl = textDir.isRtl(mChars, 0, len);
                 bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
             }
-            mLevels.resize(mTextLength);
-            mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
-            mLtrWithoutBidi = false;
-        }
-    }
-
-    private void applyReplacementRun(@NonNull ReplacementSpan replacement,
-                                     @IntRange(from = 0) int start,  // inclusive, in copied buffer
-                                     @IntRange(from = 0) int end,  // exclusive, in copied buffer
-                                     /* Maybe Zero */ long nativeStaticLayoutPtr) {
-        // Use original text. Shouldn't matter.
-        // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
-        //       backward compatibility? or Should we initialize them for getFontMetricsInt?
-        final float width = replacement.getSize(
-                mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
-        if (nativeStaticLayoutPtr == 0) {
-            // Assigns all width to the first character. This is the same behavior as minikin.
-            mWidths.set(start, width);
-            if (end > start + 1) {
-                Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
-            }
-            mWholeWidth += width;
-        } else {
-            StaticLayout.addReplacementRun(nativeStaticLayoutPtr, mCachedPaint, start, end, width);
-        }
-    }
-
-    private void applyStyleRun(@IntRange(from = 0) int start,  // inclusive, in copied buffer
-                               @IntRange(from = 0) int end,  // exclusive, in copied buffer
-                               /* Maybe Zero */ long nativeStaticLayoutPtr) {
-        if (nativeStaticLayoutPtr != 0) {
-            mCachedPaint.getFontMetricsInt(mCachedFm);
-        }
-
-        if (mLtrWithoutBidi) {
-            // If the whole text is LTR direction, just apply whole region.
-            if (nativeStaticLayoutPtr == 0) {
-                mWholeWidth += mCachedPaint.getTextRunAdvances(
-                        mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
-                        mWidths.getRawArray(), start);
-            } else {
-                StaticLayout.addStyleRun(nativeStaticLayoutPtr, mCachedPaint, start, end,
-                        false /* isRtl */);
-            }
-        } else {
-            // If there is multiple bidi levels, split into individual bidi level and apply style.
-            byte level = mLevels.get(start);
-            // Note that the empty text or empty range won't reach this method.
-            // Safe to search from start + 1.
-            for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
-                if (levelEnd == end || mLevels.get(levelEnd) != level) {  // transition point
-                    final boolean isRtl = (level & 0x1) != 0;
-                    if (nativeStaticLayoutPtr == 0) {
-                        final int levelLength = levelEnd - levelStart;
-                        mWholeWidth += mCachedPaint.getTextRunAdvances(
-                                mCopiedBuffer, levelStart, levelLength, levelStart, levelEnd, isRtl,
-                                mWidths.getRawArray(), levelStart);
-                    } else {
-                        StaticLayout.addStyleRun(
-                                nativeStaticLayoutPtr, mCachedPaint, levelStart, levelEnd, isRtl);
-                    }
-                    if (levelEnd == end) {
-                        break;
-                    }
-                    levelStart = levelEnd;
-                    level = mLevels.get(levelEnd);
-                }
-            }
-        }
-    }
-
-    private void applyMetricsAffectingSpan(
-            @NonNull TextPaint paint,
-            @Nullable MetricAffectingSpan[] spans,
-            @IntRange(from = 0) int start,  // inclusive, in original text buffer
-            @IntRange(from = 0) int end,  // exclusive, in original text buffer
-            /* Maybe Zero */ long nativeStaticLayoutPtr) {
-        mCachedPaint.set(paint);
-        // XXX paint should not have a baseline shift, but...
-        mCachedPaint.baselineShift = 0;
-
-        final boolean needFontMetrics = nativeStaticLayoutPtr != 0;
-
-        if (needFontMetrics && mCachedFm == null) {
-            mCachedFm = new Paint.FontMetricsInt();
-        }
-
-        ReplacementSpan replacement = null;
-        if (spans != null) {
-            for (int i = 0; i < spans.length; i++) {
-                MetricAffectingSpan span = spans[i];
-                if (span instanceof ReplacementSpan) {
-                    // The last ReplacementSpan is effective for backward compatibility reasons.
-                    replacement = (ReplacementSpan) span;
-                } else {
-                    // TODO: No need to call updateMeasureState for ReplacementSpan as well?
-                    span.updateMeasureState(mCachedPaint);
-                }
-            }
-        }
-
-        final int startInCopiedBuffer = start - mTextStart;
-        final int endInCopiedBuffer = end - mTextStart;
-
-        if (replacement != null) {
-            applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
-                    nativeStaticLayoutPtr);
-        } else {
-            applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeStaticLayoutPtr);
-        }
-
-        if (needFontMetrics) {
-            if (mCachedPaint.baselineShift < 0) {
-                mCachedFm.ascent += mCachedPaint.baselineShift;
-                mCachedFm.top += mCachedPaint.baselineShift;
-            } else {
-                mCachedFm.descent += mCachedPaint.baselineShift;
-                mCachedFm.bottom += mCachedPaint.baselineShift;
-            }
-
-            mFontMetrics.append(mCachedFm.top);
-            mFontMetrics.append(mCachedFm.bottom);
-            mFontMetrics.append(mCachedFm.ascent);
-            mFontMetrics.append(mCachedFm.descent);
+            mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels);
+            mEasy = false;
         }
     }
 
     /**
-     * Returns the maximum index that the accumulated width not exceeds the width.
+     * Apply the style.
      *
-     * If forward=false is passed, returns the minimum index from the end instead.
-     *
-     * This only works if the MeasuredText is computed with computeForMeasurement.
-     * Undefined behavior in other case.
+     * If nativeStaticLayoutPtr is 0, this method measures the styled text width.
+     * If nativeStaticLayoutPtr is not 0, this method just passes the style information to native
+     * code by calling StaticLayout.addstyleRun() and returns 0.
      */
-    @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
-        float[] w = mWidths.getRawArray();
+    float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm,
+            long nativeStaticLayoutPtr) {
+        if (fm != null) {
+            paint.getFontMetricsInt(fm);
+        }
+
+        final int p = mPos;
+        mPos = p + len;
+
+        if (mEasy) {
+            final boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT;
+            if (nativeStaticLayoutPtr == 0) {
+                return paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, mWidths, p);
+            } else {
+                StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, p, p + len, isRtl);
+                return 0.0f;  // Builder.addStyleRun doesn't return the width.
+            }
+        }
+
+        float totalAdvance = 0;
+        int level = mLevels[p];
+        for (int q = p, i = p + 1, e = p + len;; ++i) {
+            if (i == e || mLevels[i] != level) {
+                final boolean isRtl = (level & 0x1) != 0;
+                if (nativeStaticLayoutPtr == 0) {
+                    totalAdvance +=
+                            paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, mWidths, q);
+                } else {
+                    // Builder.addStyleRun doesn't return the width.
+                    StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, q, i, isRtl);
+                }
+                if (i == e) {
+                    break;
+                }
+                q = i;
+                level = mLevels[i];
+            }
+        }
+        return totalAdvance;  // If nativeStaticLayoutPtr is 0, the result is zero.
+    }
+
+    float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) {
+        return addStyleRun(paint, len, fm, 0 /* native ptr */);
+    }
+
+    float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
+            Paint.FontMetricsInt fm, long nativeStaticLayoutPtr) {
+
+        TextPaint workPaint = mWorkPaint;
+        workPaint.set(paint);
+        // XXX paint should not have a baseline shift, but...
+        workPaint.baselineShift = 0;
+
+        ReplacementSpan replacement = null;
+        for (int i = 0; i < spans.length; i++) {
+            MetricAffectingSpan span = spans[i];
+            if (span instanceof ReplacementSpan) {
+                replacement = (ReplacementSpan)span;
+            } else {
+                span.updateMeasureState(workPaint);
+            }
+        }
+
+        float wid;
+        if (replacement == null) {
+            wid = addStyleRun(workPaint, len, fm, nativeStaticLayoutPtr);
+        } else {
+            // Use original text.  Shouldn't matter.
+            wid = replacement.getSize(workPaint, mText, mTextStart + mPos,
+                    mTextStart + mPos + len, fm);
+            if (nativeStaticLayoutPtr == 0) {
+                float[] w = mWidths;
+                w[mPos] = wid;
+                for (int i = mPos + 1, e = mPos + len; i < e; i++)
+                    w[i] = 0;
+            } else {
+                StaticLayout.addReplacementRun(nativeStaticLayoutPtr, paint, mPos, mPos + len, wid);
+            }
+            mPos += len;
+        }
+
+        if (fm != null) {
+            if (workPaint.baselineShift < 0) {
+                fm.ascent += workPaint.baselineShift;
+                fm.top += workPaint.baselineShift;
+            } else {
+                fm.descent += workPaint.baselineShift;
+                fm.bottom += workPaint.baselineShift;
+            }
+        }
+
+        return wid;
+    }
+
+    float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
+            Paint.FontMetricsInt fm) {
+        return addStyleRun(paint, spans, len, fm, 0 /* native ptr */);
+    }
+
+    int breakText(int limit, boolean forwards, float width) {
+        float[] w = mWidths;
         if (forwards) {
             int i = 0;
             while (i < limit) {
@@ -550,7 +267,7 @@
                 if (width < 0.0f) break;
                 i++;
             }
-            while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
+            while (i > 0 && mChars[i - 1] == ' ') i--;
             return i;
         } else {
             int i = limit - 1;
@@ -559,22 +276,16 @@
                 if (width < 0.0f) break;
                 i--;
             }
-            while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
+            while (i < limit - 1 && (mChars[i + 1] == ' ' || w[i + 1] == 0.0f)) {
                 i++;
             }
             return limit - i - 1;
         }
     }
 
-    /**
-     * Returns the length of the substring.
-     *
-     * This only works if the MeasuredText is computed with computeForMeasurement.
-     * Undefined behavior in other case.
-     */
-    @FloatRange(from = 0.0f) float measure(int start, int limit) {
+    float measure(int start, int limit) {
         float width = 0;
-        float[] w = mWidths.getRawArray();
+        float[] w = mWidths;
         for (int i = start; i < limit; ++i) {
             width += w[i];
         }
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 81c82c9..c0fc44f 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -21,10 +21,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Paint;
-import android.text.AutoGrowArray.FloatArray;
 import android.text.style.LeadingMarginSpan;
 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
 import android.text.style.LineHeightSpan;
+import android.text.style.MetricAffectingSpan;
 import android.text.style.TabStopSpan;
 import android.util.Log;
 import android.util.Pools.SynchronizedPool;
@@ -99,6 +99,8 @@
             b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
             b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
             b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
+
+            b.mMeasuredText = MeasuredText.obtain();
             return b;
         }
 
@@ -109,6 +111,8 @@
         private static void recycle(@NonNull Builder b) {
             b.mPaint = null;
             b.mText = null;
+            MeasuredText.recycle(b.mMeasuredText);
+            b.mMeasuredText = null;
             b.mLeftIndents = null;
             b.mRightIndents = null;
             b.mLeftPaddings = null;
@@ -124,6 +128,7 @@
             mRightIndents = null;
             mLeftPaddings = null;
             mRightPaddings = null;
+            mMeasuredText.finish();
         }
 
         public Builder setText(CharSequence source) {
@@ -439,6 +444,9 @@
 
         private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
 
+        // This will go away and be subsumed by native builder code
+        private MeasuredText mMeasuredText;
+
         private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
     }
 
@@ -610,7 +618,11 @@
         TextUtils.TruncateAt ellipsize = b.mEllipsize;
         final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
         LineBreaks lineBreaks = new LineBreaks();  // TODO: move to builder to avoid allocation costs
-        FloatArray widths = new FloatArray();
+        // store span end locations
+        int[] spanEndCache = new int[4];
+        // store fontMetrics per span range
+        // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range)
+        int[] fmCache = new int[4 * 4];
 
         mLineCount = 0;
         mEllipsized = false;
@@ -622,6 +634,8 @@
         Paint.FontMetricsInt fm = b.mFontMetricsInt;
         int[] chooseHtv = null;
 
+        MeasuredText measured = b.mMeasuredText;
+
         Spanned spanned = null;
         if (source instanceof Spanned)
             spanned = (Spanned) source;
@@ -648,7 +662,6 @@
                 b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
                 indents, mLeftPaddings, mRightPaddings);
 
-        MeasuredText measured = null;
         try {
             int paraEnd;
             for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
@@ -708,6 +721,13 @@
                     }
                 }
 
+                measured.setPara(source, paraStart, paraEnd, textDir);
+                char[] chs = measured.mChars;
+                float[] widths = measured.mWidths;
+                byte[] chdirs = measured.mLevels;
+                int dir = measured.mDir;
+                boolean easy = measured.mEasy;
+
                 // tab stop locations
                 int[] variableTabStops = null;
                 if (spanned != null) {
@@ -723,16 +743,50 @@
                     }
                 }
 
-                measured = MeasuredText.buildForStaticLayout(
-                        paint, source, paraStart, paraEnd, textDir, nativePtr, measured);
-                final char[] chs = measured.getChars();
-                final int[] spanEndCache = measured.getSpanEndCache().getRawArray();
-                final int[] fmCache = measured.getFontMetrics().getRawArray();
-                widths.resize(chs.length);
-
                 // measurement has to be done before performing line breaking
                 // but we don't want to recompute fontmetrics or span ranges the
                 // second time, so we cache those and then use those stored values
+                int fmCacheCount = 0;
+                int spanEndCacheCount = 0;
+                for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
+                    if (fmCacheCount * 4 >= fmCache.length) {
+                        int[] grow = new int[fmCacheCount * 4 * 2];
+                        System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
+                        fmCache = grow;
+                    }
+
+                    if (spanEndCacheCount >= spanEndCache.length) {
+                        int[] grow = new int[spanEndCacheCount * 2];
+                        System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
+                        spanEndCache = grow;
+                    }
+
+                    if (spanned == null) {
+                        spanEnd = paraEnd;
+                        int spanLen = spanEnd - spanStart;
+                        measured.addStyleRun(paint, spanLen, fm, nativePtr);
+                    } else {
+                        spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
+                                MetricAffectingSpan.class);
+                        int spanLen = spanEnd - spanStart;
+                        MetricAffectingSpan[] spans =
+                                spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
+                        spans = TextUtils.removeEmptySpans(spans, spanned,
+                                MetricAffectingSpan.class);
+                        measured.addStyleRun(paint, spans, spanLen, fm, nativePtr);
+                    }
+
+                    // the order of storage here (top, bottom, ascent, descent) has to match the
+                    // code below where these values are retrieved
+                    fmCache[fmCacheCount * 4 + 0] = fm.top;
+                    fmCache[fmCacheCount * 4 + 1] = fm.bottom;
+                    fmCache[fmCacheCount * 4 + 2] = fm.ascent;
+                    fmCache[fmCacheCount * 4 + 3] = fm.descent;
+                    fmCacheCount++;
+
+                    spanEndCache[spanEndCacheCount] = spanEnd;
+                    spanEndCacheCount++;
+                }
 
                 int breakCount = nComputeLineBreaks(
                         nativePtr,
@@ -755,7 +809,7 @@
                         lineBreaks.ascents,
                         lineBreaks.descents,
                         lineBreaks.flags,
-                        widths.getRawArray());
+                        widths);
 
                 final int[] breaks = lineBreaks.breaks;
                 final float[] lineWidths = lineBreaks.widths;
@@ -778,7 +832,7 @@
                             width += lineWidths[i];
                         } else {
                             for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
-                                width += widths.get(j);
+                                width += widths[j];
                             }
                         }
                         flag |= flags[i] & TAB_MASK;
@@ -842,10 +896,10 @@
                         v = out(source, here, endPos,
                                 ascent, descent, fmTop, fmBottom,
                                 v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
-                                flags[breakIndex], needMultiply, measured, bufEnd,
-                                includepad, trackpad, addLastLineSpacing, chs, widths.getRawArray(),
-                                paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
-                                paint, moreChars);
+                                flags[breakIndex], needMultiply, chdirs, dir, easy, bufEnd,
+                                includepad, trackpad, addLastLineSpacing, chs, widths, paraStart,
+                                ellipsize, ellipsizedWidth, lineWidths[breakIndex], paint,
+                                moreChars);
 
                         if (endPos < spanEnd) {
                             // preserve metrics for current span
@@ -873,8 +927,7 @@
 
             if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
                     && mLineCount < mMaximumVisibleLineCount) {
-                measured = MeasuredText.buildForStaticLayout(
-                        paint, source, bufEnd, bufEnd, textDir, nativePtr, measured);
+                measured.setPara(source, bufEnd, bufEnd, textDir);
 
                 paint.getFontMetricsInt(fm);
 
@@ -884,15 +937,12 @@
                         v,
                         spacingmult, spacingadd, null,
                         null, fm, 0,
-                        needMultiply, measured, bufEnd,
+                        needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
                         includepad, trackpad, addLastLineSpacing, null,
                         null, bufStart, ellipsize,
                         ellipsizedWidth, 0, paint, false);
             }
         } finally {
-            if (measured != null) {
-                measured.recycle();
-            }
             nFinish(nativePtr);
         }
     }
@@ -902,8 +952,8 @@
     private int out(final CharSequence text, final int start, final int end, int above, int below,
             int top, int bottom, int v, final float spacingmult, final float spacingadd,
             final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
-            final int flags, final boolean needMultiply, final MeasuredText measured,
-            final int bufEnd, final boolean includePad, final boolean trackPad,
+            final int flags, final boolean needMultiply, final byte[] chdirs, final int dir,
+            final boolean easy, final int bufEnd, final boolean includePad, final boolean trackPad,
             final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
             final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
             final float textWidth, final TextPaint paint, final boolean moreChars) {
@@ -911,7 +961,6 @@
         final int off = j * mColumns;
         final int want = off + mColumns + TOP;
         int[] lines = mLines;
-        final int dir = measured.getParagraphDir();
 
         if (want >= lines.length) {
             final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
@@ -937,8 +986,17 @@
         // one bit for start field
         lines[off + TAB] |= flags & TAB_MASK;
         lines[off + HYPHEN] = flags;
+
         lines[off + DIR] |= dir << DIR_SHIFT;
-        mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
+        // easy means all chars < the first RTL, so no emoji, no nothing
+        // XXX a run with no text or all spaces is easy but might be an empty
+        // RTL paragraph.  Make sure easy is false if this is the case.
+        if (easy) {
+            mLineDirections[j] = DIRS_ALL_LEFT_TO_RIGHT;
+        } else {
+            mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
+                    start - widthStart, end - start);
+        }
 
         final boolean firstLine = (j == 0);
         final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index af66157..cbdaa69 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -42,6 +42,7 @@
 import android.text.style.ForegroundColorSpan;
 import android.text.style.LeadingMarginSpan;
 import android.text.style.LocaleSpan;
+import android.text.style.MetricAffectingSpan;
 import android.text.style.ParagraphStyle;
 import android.text.style.QuoteSpan;
 import android.text.style.RelativeSizeSpan;
@@ -1250,11 +1251,10 @@
             @NonNull String ellipsis) {
 
         final int len = text.length();
-        MeasuredText mt = null;
+        final MeasuredText mt = MeasuredText.obtain();
         MeasuredText resultMt = null;
         try {
-            mt = MeasuredText.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
-            float width = mt.getWholeWidth();
+            float width = setPara(mt, paint, text, 0, text.length(), textDir);
 
             if (width <= avail) {
                 if (callback != null) {
@@ -1263,6 +1263,7 @@
                 return text;
             }
 
+            resultMt = MeasuredText.obtain();
             // First estimate of effective width of ellipsis.
             float ellipsisWidth = paint.measureText(ellipsis);
             int numberOfTries = 0;
@@ -1289,7 +1290,7 @@
                     }
                 }
 
-                final char[] buf = mt.getChars();
+                final char[] buf = mt.mChars;
                 final Spanned sp = text instanceof Spanned ? (Spanned) text : null;
 
                 final int removed = end - start;
@@ -1332,9 +1333,7 @@
                 if (remaining == 0) { // All text is gone.
                     textFits = true;
                 } else {
-                    resultMt = MeasuredText.buildForMeasurement(
-                            paint, result, 0, result.length(), textDir, resultMt);
-                    width = resultMt.getWholeWidth();
+                    width = setPara(resultMt, paint, result, 0, result.length(), textDir);
                     if (width <= avail) {
                         textFits = true;
                     } else {
@@ -1358,11 +1357,9 @@
             }
             return result;
         } finally {
-            if (mt != null) {
-                mt.recycle();
-            }
+            MeasuredText.recycle(mt);
             if (resultMt != null) {
-                resultMt.recycle();
+                MeasuredText.recycle(resultMt);
             }
         }
     }
@@ -1479,17 +1476,15 @@
     public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
          float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
 
-        MeasuredText mt = null;
-        MeasuredText tempMt = null;
+        MeasuredText mt = MeasuredText.obtain();
         try {
             int len = text.length();
-            mt = MeasuredText.buildForMeasurement(p, text, 0, len, textDir, mt);
-            float width = mt.getWholeWidth();
+            float width = setPara(mt, p, text, 0, len, textDir);
             if (width <= avail) {
                 return text;
             }
 
-            char[] buf = mt.getChars();
+            char[] buf = mt.mChars;
 
             int commaCount = 0;
             for (int i = 0; i < len; i++) {
@@ -1505,8 +1500,9 @@
 
             int w = 0;
             int count = 0;
-            float[] widths = mt.getWidths().getRawArray();
+            float[] widths = mt.mWidths;
 
+            MeasuredText tempMt = MeasuredText.obtain();
             for (int i = 0; i < len; i++) {
                 w += widths[i];
 
@@ -1523,9 +1519,8 @@
                     }
 
                     // XXX this is probably ok, but need to look at it more
-                    tempMt = MeasuredText.buildForMeasurement(
-                            p, format, 0, format.length(), textDir, tempMt);
-                    float moreWid = tempMt.getWholeWidth();
+                    tempMt.setPara(format, 0, format.length(), textDir);
+                    float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null);
 
                     if (w + moreWid <= avail) {
                         ok = i + 1;
@@ -1533,18 +1528,40 @@
                     }
                 }
             }
+            MeasuredText.recycle(tempMt);
 
             SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
             out.insert(0, text, 0, ok);
             return out;
         } finally {
-            if (mt != null) {
-                mt.recycle();
-            }
-            if (tempMt != null) {
-                tempMt.recycle();
+            MeasuredText.recycle(mt);
+        }
+    }
+
+    private static float setPara(MeasuredText mt, TextPaint paint,
+            CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
+
+        mt.setPara(text, start, end, textDir);
+
+        float width;
+        Spanned sp = text instanceof Spanned ? (Spanned) text : null;
+        int len = end - start;
+        if (sp == null) {
+            width = mt.addStyleRun(paint, len, null);
+        } else {
+            width = 0;
+            int spanEnd;
+            for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
+                spanEnd = sp.nextSpanTransition(spanStart, len,
+                        MetricAffectingSpan.class);
+                MetricAffectingSpan[] spans = sp.getSpans(
+                        spanStart, spanEnd, MetricAffectingSpan.class);
+                spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class);
+                width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
             }
         }
+
+        return width;
     }
 
     // Returns true if the character's presence could affect RTL layout.
diff --git a/core/java/android/util/StatsManager.java b/core/java/android/util/StatsManager.java
index 2bcd863..26a3c36 100644
--- a/core/java/android/util/StatsManager.java
+++ b/core/java/android/util/StatsManager.java
@@ -93,10 +93,11 @@
     }
 
     /**
-     * Clients can request data with a binder call.
+     * Clients can request data with a binder call. This getter is destructive and also clears
+     * the retrieved metrics from statsd memory.
      *
      * @param configKey Configuration key to retrieve data from.
-     * @return Serialized ConfigMetricsReport proto. Returns null on failure.
+     * @return Serialized ConfigMetricsReportList proto. Returns null on failure.
      */
     @RequiresPermission(Manifest.permission.DUMP)
     public byte[] getData(String configKey) {
@@ -115,6 +116,30 @@
         }
     }
 
+    /**
+     * Clients can request metadata for statsd. Will contain stats across all configurations but not
+     * the actual metrics themselves (metrics must be collected via {@link #getData(String)}.
+     * This getter is not destructive and will not reset any metrics/counters.
+     *
+     * @return Serialized StatsdStatsReport proto. Returns null on failure.
+     */
+    @RequiresPermission(Manifest.permission.DUMP)
+    public byte[] getMetadata() {
+        synchronized (this) {
+            try {
+                IStatsManager service = getIStatsManagerLocked();
+                if (service == null) {
+                    Slog.d(TAG, "Failed to find statsd when getting metadata");
+                    return null;
+                }
+                return service.getMetadata();
+            } catch (RemoteException e) {
+                Slog.d(TAG, "Failed to connecto statsd when getting metadata");
+                return null;
+            }
+        }
+    }
+
     private class StatsdDeathRecipient implements IBinder.DeathRecipient {
         @Override
         public void binderDied() {
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 547e0db..9a99e53 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -24,6 +24,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemService;
+import android.annotation.TestApi;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -36,6 +37,7 @@
 import android.os.RemoteException;
 import android.service.autofill.AutofillService;
 import android.service.autofill.FillEventHistory;
+import android.service.autofill.UserData;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
@@ -1007,6 +1009,54 @@
     }
 
     /**
+     * Gets the user data used for <a href="#FieldsClassification">fields classification</a>.
+     *
+     * <p><b>Note:</b> This method should only be called by an app providing an autofill service.
+     *
+     * TODO(b/67867469):
+     *  - proper javadoc
+     *  - unhide / remove testApi
+     *
+     * @return value previously set by {@link #setUserData(UserData)} or {@code null} if it was
+     * reset or if the caller currently does not have an enabled autofill service for the user.
+     *
+     * @hide
+     */
+    @TestApi
+    @Nullable public UserData getUserData() {
+        try {
+            return mService.getUserData();
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return null;
+        }
+    }
+
+    /**
+     * Sets the user data used for <a href="#FieldsClassification">fields classification</a>.
+     *
+     * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+     * and it's ignored if the caller currently doesn't have an enabled autofill service for
+     * the user.
+     *
+     * TODO(b/67867469):
+     *  - proper javadoc
+     *  - unhide / remove testApi
+     *  - add unit tests:
+     *    - call set / get / verify
+     *
+     * @hide
+     */
+    @TestApi
+    public void setUserData(@Nullable UserData userData) {
+        try {
+            mService.setUserData(userData);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Returns {@code true} if autofill is supported by the current device and
      * is supported for this user.
      *
diff --git a/core/java/android/view/autofill/AutofillValue.java b/core/java/android/view/autofill/AutofillValue.java
index 3beae11..8e649de 100644
--- a/core/java/android/view/autofill/AutofillValue.java
+++ b/core/java/android/view/autofill/AutofillValue.java
@@ -177,7 +177,7 @@
                 .append("[type=").append(mType)
                 .append(", value=");
         if (isText()) {
-            string.append(((CharSequence) mValue).length()).append("_chars");
+            Helper.appendRedacted(string, (CharSequence) mValue);
         } else {
             string.append(mValue);
         }
diff --git a/core/java/android/view/autofill/Helper.java b/core/java/android/view/autofill/Helper.java
index 829e7f3..b95704a 100644
--- a/core/java/android/view/autofill/Helper.java
+++ b/core/java/android/view/autofill/Helper.java
@@ -16,6 +16,8 @@
 
 package android.view.autofill;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Bundle;
 
 import java.util.Arrays;
@@ -50,6 +52,35 @@
         return builder;
     }
 
+    /**
+     * Appends {@code value} to the {@code builder} redacting its contents.
+     */
+    public static void appendRedacted(@NonNull StringBuilder builder,
+            @Nullable CharSequence value) {
+        if (value == null) {
+            builder.append("null");
+        } else {
+            builder.append(value.length()).append("_chars");
+        }
+    }
+
+    /**
+     * Appends {@code values} to the {@code builder} redacting its contents.
+     */
+    public static void appendRedacted(@NonNull StringBuilder builder, @Nullable String[] values) {
+        if (values == null) {
+            builder.append("N/A");
+            return;
+        }
+        builder.append("[");
+        for (String value : values) {
+            builder.append(" '");
+            appendRedacted(builder, value);
+            builder.append("'");
+        }
+        builder.append(" ]");
+    }
+
     private Helper() {
         throw new UnsupportedOperationException("contains static members only");
     }
diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl
index d6db3fe..7d6a19f 100644
--- a/core/java/android/view/autofill/IAutoFillManager.aidl
+++ b/core/java/android/view/autofill/IAutoFillManager.aidl
@@ -21,6 +21,7 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.service.autofill.FillEventHistory;
+import android.service.autofill.UserData;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillValue;
 import android.view.autofill.IAutoFillManagerClient;
@@ -53,4 +54,6 @@
     boolean isServiceSupported(int userId);
     boolean isServiceEnabled(int userId, String packageName);
     void onPendingSaveUi(int operation, IBinder token);
+    UserData getUserData();
+    void setUserData(in UserData userData);
 }
diff --git a/core/jni/android/graphics/YuvToJpegEncoder.cpp b/core/jni/android/graphics/YuvToJpegEncoder.cpp
index 31567f7..5eecd9c 100644
--- a/core/jni/android/graphics/YuvToJpegEncoder.cpp
+++ b/core/jni/android/graphics/YuvToJpegEncoder.cpp
@@ -26,12 +26,13 @@
 bool YuvToJpegEncoder::encode(SkWStream* stream, void* inYuv, int width,
         int height, int* offsets, int jpegQuality) {
     jpeg_compress_struct    cinfo;
-    skjpeg_error_mgr        sk_err;
+    jpeg_error_mgr          err;
     skjpeg_destination_mgr  sk_wstream(stream);
 
-    cinfo.err = jpeg_std_error(&sk_err);
-    sk_err.error_exit = skjpeg_error_exit;
-    if (setjmp(sk_err.fJmpBuf)) {
+    cinfo.err = jpeg_std_error(&err);
+    err.error_exit = skjpeg_error_exit;
+    jmp_buf jmp;
+    if (setjmp(jmp)) {
         return false;
     }
     jpeg_create_compress(&cinfo);
diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp
index 870a0c2..519a885 100644
--- a/core/jni/android_view_ThreadedRenderer.cpp
+++ b/core/jni/android_view_ThreadedRenderer.cpp
@@ -1043,7 +1043,22 @@
     { "nSetHighContrastText", "(Z)V", (void*)android_view_ThreadedRenderer_setHighContrastText },
 };
 
+static JavaVM* mJvm = nullptr;
+
+static void attachRenderThreadToJvm() {
+    LOG_ALWAYS_FATAL_IF(!mJvm, "No jvm but we set the hook??");
+
+    JavaVMAttachArgs args;
+    args.version = JNI_VERSION_1_4;
+    args.name = (char*) "RenderThread";
+    args.group = NULL;
+    JNIEnv* env;
+    mJvm->AttachCurrentThreadAsDaemon(&env, (void*) &args);
+}
+
 int register_android_view_ThreadedRenderer(JNIEnv* env) {
+    env->GetJavaVM(&mJvm);
+    RenderThread::setOnStartHook(&attachRenderThreadToJvm);
     jclass observerClass = FindClassOrDie(env, "android/view/FrameMetricsObserver");
     gFrameMetricsObserverClassInfo.frameMetrics = GetFieldIDOrDie(
             env, observerClass, "mFrameMetrics", "Landroid/view/FrameMetrics;");
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 4e0e5ed..0367275 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -418,7 +418,13 @@
     private static final Set<String> BACKUP_BLACKLISTED_SECURE_SETTINGS =
              newHashSet(
                  Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE,
+                 // TODO(b/67867469): Move autofill settings below to
+                 // BACKUP_BLACKLISTED_SYSTEM_SETTINGS once feature is moved out of experimental
                  Settings.Secure.AUTOFILL_FEATURE_FIELD_DETECTION,
+                 Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
+                 Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE,
+                 Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH,
+                 Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH,
                  Settings.Secure.ALLOWED_GEOLOCATION_ORIGINS,
                  Settings.Secure.ALWAYS_ON_VPN_APP,
                  Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN,
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index 05a9b75..20443ec 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -51,10 +51,17 @@
 
 static bool gHasRenderThreadInstance = false;
 
+static void (*gOnStartHook)() = nullptr;
+
 bool RenderThread::hasInstance() {
     return gHasRenderThreadInstance;
 }
 
+void RenderThread::setOnStartHook(void (*onStartHook)()) {
+    LOG_ALWAYS_FATAL_IF(hasInstance(), "can't set an onStartHook after we've started...");
+    gOnStartHook = onStartHook;
+}
+
 RenderThread& RenderThread::getInstance() {
     // This is a pointer because otherwise __cxa_finalize
     // will try to delete it like a Good Citizen but that causes us to crash
@@ -256,6 +263,9 @@
 
 bool RenderThread::threadLoop() {
     setpriority(PRIO_PROCESS, 0, PRIORITY_DISPLAY);
+    if (gOnStartHook) {
+        gOnStartHook();
+    }
     initThreadLocals();
 
     while (true) {
diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h
index d17a509..970537b 100644
--- a/libs/hwui/renderthread/RenderThread.h
+++ b/libs/hwui/renderthread/RenderThread.h
@@ -67,6 +67,9 @@
     PREVENT_COPY_AND_ASSIGN(RenderThread);
 
 public:
+    // Sets a callback that fires before any RenderThread setup has occured.
+    ANDROID_API static void setOnStartHook(void (*onStartHook)());
+
     WorkQueue& queue() { return ThreadBase::queue(); }
 
     // Mimics android.view.Choreographer
diff --git a/libs/hwui/thread/ThreadBase.h b/libs/hwui/thread/ThreadBase.h
index b3fec1f..8068121 100644
--- a/libs/hwui/thread/ThreadBase.h
+++ b/libs/hwui/thread/ThreadBase.h
@@ -31,7 +31,10 @@
     PREVENT_COPY_AND_ASSIGN(ThreadBase);
 
 public:
-    ThreadBase() : mLooper(new Looper(false)), mQueue([this]() { mLooper->wake(); }, mLock) {}
+    ThreadBase()
+            : Thread(false)
+            , mLooper(new Looper(false))
+            , mQueue([this]() { mLooper->wake(); }, mLock) {}
 
     WorkQueue& queue() { return mQueue; }
 
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index fac254a..863f17b 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.systemui.HardwareUiLayout
+<RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -45,4 +45,4 @@
         </LinearLayout>
 
     </RelativeLayout>
-</com.android.systemui.HardwareUiLayout>
+</RelativeLayout>
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 383d327..f16d7b8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -19,6 +19,9 @@
 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
 
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE;
 import static com.android.systemui.volume.Events.DISMISS_REASON_TOUCH_OUTSIDE;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
@@ -42,6 +45,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemClock;
+import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.util.Log;
 import android.util.Slog;
@@ -67,6 +71,7 @@
 
 import com.android.settingslib.Utils;
 import com.android.systemui.Dependency;
+import com.android.systemui.HardwareBgDrawable;
 import com.android.systemui.HardwareUiLayout;
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
@@ -74,6 +79,7 @@
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.plugins.VolumeDialogController.State;
 import com.android.systemui.plugins.VolumeDialogController.StreamState;
+import com.android.systemui.util.leak.RotationUtils;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -97,9 +103,13 @@
     private final VolumeDialogController mController;
 
     private Window mWindow;
-    private HardwareUiLayout mHardwareLayout;
+    //private HardwareUiLayout mHardwareLayout;
     private CustomDialog mDialog;
     private ViewGroup mDialogView;
+    private boolean mEdgeBleed;
+    private boolean mRoundedDivider;
+    private HardwareBgDrawable mBackground;
+    private int mRotation = ROTATION_NONE;
     private ViewGroup mDialogRowsView;
     private ViewGroup mDialogContentView;
     private final List<VolumeRow> mRows = new ArrayList<>();
@@ -111,6 +121,8 @@
     private final Accessibility mAccessibility = new Accessibility();
     private final ColorStateList mActiveSliderTint;
     private final ColorStateList mInactiveSliderTint;
+    private static final String EDGE_BLEED = "sysui_hwui_edge_bleed";
+    private static final String ROUNDED_DIVIDER = "sysui_hwui_rounded_divider";
 
     private boolean mShowing;
     private boolean mShowA11yStream;
@@ -181,8 +193,16 @@
                 return true;
             }
         });
-        mHardwareLayout = HardwareUiLayout.get(mDialogView);
-        mHardwareLayout.setOutsideTouchListener(view -> dismiss(DISMISS_REASON_TOUCH_OUTSIDE));
+
+        mEdgeBleed = Settings.Secure.getInt(mContext.getContentResolver(),
+                EDGE_BLEED, 0) != 0;
+        mRoundedDivider = Settings.Secure.getInt(mContext.getContentResolver(),
+                ROUNDED_DIVIDER, 1) != 0;
+        updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding());
+        mBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed, mContext);
+        mDialogView.setBackground(mBackground);
+        //mHardwareLayout = HardwareUiLayout.get(mDialogView);
+        //mHardwareLayout.setOutsideTouchListener(view -> dismiss(DISMISS_REASON_TOUCH_OUTSIDE));
 
         mDialogContentView = mDialog.findViewById(R.id.volume_dialog_content);
         mDialogRowsView = mDialogContentView.findViewById(R.id.volume_dialog_rows);
@@ -210,6 +230,25 @@
         updateRowsH(getActiveRow());
     }
 
+    private int getEdgePadding() {
+        return mContext.getResources().getDimensionPixelSize(R.dimen.edge_margin);
+    }
+
+    private void updateEdgeMargin(int edge) {
+        if (mDialogView != null) {
+            mRotation = RotationUtils.getRotation(mContext);
+            ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mDialogView.getLayoutParams();
+            if (mRotation == ROTATION_LANDSCAPE) {
+                params.topMargin = edge;
+            } else if (mRotation == ROTATION_SEASCAPE) {
+                params.bottomMargin = edge;
+            } else {
+                params.rightMargin = edge;
+            }
+            mDialogView.setLayoutParams(params);
+        }
+    }
+
     private ColorStateList loadColorStateList(int colorResId) {
         return ColorStateList.valueOf(mContext.getColor(colorResId));
     }
@@ -389,11 +428,11 @@
         rescheduleTimeoutH();
         if (mShowing) return;
         mShowing = true;
-        mHardwareLayout.setTranslationX(getAnimTranslation());
-        mHardwareLayout.setAlpha(0);
-        mHardwareLayout.animate()
+        mDialogView.setTranslationY(getAnimTranslation());
+        mDialogView.setAlpha(0);
+        mDialogView.animate()
                 .alpha(1)
-                .translationX(0)
+                .translationY(0)
                 .setDuration(300)
                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .withEndAction(() -> {
@@ -432,9 +471,9 @@
         mHandler.removeMessages(H.SHOW);
         if (!mShowing) return;
         mShowing = false;
-        mHardwareLayout.setTranslationX(0);
-        mHardwareLayout.setAlpha(1);
-        mHardwareLayout.animate()
+        mDialogView.setTranslationX(0);
+        mDialogView.setAlpha(1);
+        mDialogView.animate()
                 .alpha(0)
                 .translationX(getAnimTranslation())
                 .setDuration(300)
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index 23e4f50..0291276 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -52,6 +52,7 @@
 import android.os.UserManagerInternal;
 import android.provider.Settings;
 import android.service.autofill.FillEventHistory;
+import android.service.autofill.UserData;
 import android.util.LocalLog;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -581,6 +582,34 @@
         }
 
         @Override
+        public UserData getUserData() throws RemoteException {
+            UserHandle user = getCallingUserHandle();
+            int uid = getCallingUid();
+
+            synchronized (mLock) {
+                AutofillManagerServiceImpl service = peekServiceForUserLocked(user.getIdentifier());
+                if (service != null) {
+                    return service.getUserData(uid);
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        public void setUserData(UserData userData) throws RemoteException {
+            UserHandle user = getCallingUserHandle();
+            int uid = getCallingUid();
+
+            synchronized (mLock) {
+                AutofillManagerServiceImpl service = peekServiceForUserLocked(user.getIdentifier());
+                if (service != null) {
+                    service.setUserData(uid, userData);
+                }
+            }
+        }
+
+        @Override
         public boolean restoreSession(int sessionId, IBinder activityToken, IBinder appCallback)
                 throws RemoteException {
             activityToken = Preconditions.checkNotNull(activityToken, "activityToken");
@@ -723,6 +752,7 @@
             }
 
             boolean oldDebug = sDebug;
+            final String prefix = "  ";
             try {
                 synchronized (mLock) {
                     oldDebug = sDebug;
@@ -731,6 +761,7 @@
                     pw.print("Verbose mode: "); pw.println(sVerbose);
                     pw.print("Disabled users: "); pw.println(mDisabledUsers);
                     pw.print("Max partitions per session: "); pw.println(sPartitionMaxCount);
+                    pw.println("User data constraints: "); UserData.dumpConstraints(prefix, pw);
                     final int size = mServicesCache.size();
                     pw.print("Cached services: ");
                     if (size == 0) {
@@ -740,7 +771,7 @@
                         for (int i = 0; i < size; i++) {
                             pw.print("\nService at index "); pw.println(i);
                             final AutofillManagerServiceImpl impl = mServicesCache.valueAt(i);
-                            impl.dumpLocked("  ", pw);
+                            impl.dumpLocked(prefix, pw);
                         }
                     }
                     mUi.dump(pw);
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 21e2722..8b6dc20 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -51,6 +51,7 @@
 import android.service.autofill.FillEventHistory.Event;
 import android.service.autofill.FillResponse;
 import android.service.autofill.IAutoFillService;
+import android.service.autofill.UserData;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -121,6 +122,11 @@
     private boolean mDisabled;
 
     /**
+     * Data used for field classification.
+     */
+    private UserData mUserData;
+
+    /**
      * Caches whether the setup completed for the current user.
      */
     @GuardedBy("mLock")
@@ -183,6 +189,14 @@
         }
     }
 
+    private int getServiceUidLocked() {
+        if (mInfo == null) {
+            Slog.w(TAG,  "getServiceUidLocked(): no mInfo");
+            return -1;
+        }
+        return mInfo.getServiceInfo().applicationInfo.uid;
+    }
+
     @Nullable
     String getServicePackageName() {
         final ComponentName serviceComponent = getServiceComponentName();
@@ -574,9 +588,9 @@
      * Initializes the last fill selection after an autofill service returned a new
      * {@link FillResponse}.
      */
-    void setLastResponse(int serviceUid, int sessionId, @NonNull FillResponse response) {
+    void setLastResponse(int sessionId, @NonNull FillResponse response) {
         synchronized (mLock) {
-            mEventHistory = new FillEventHistory(serviceUid, sessionId, response.getClientState());
+            mEventHistory = new FillEventHistory(sessionId, response.getClientState());
         }
     }
 
@@ -688,18 +702,54 @@
      */
     FillEventHistory getFillEventHistory(int callingUid) {
         synchronized (mLock) {
-            if (mEventHistory != null && mEventHistory.getServiceUid() == callingUid) {
+            if (mEventHistory != null
+                    && isCalledByServiceLocked("getFillEventHistory", callingUid)) {
                 return mEventHistory;
             }
         }
-
         return null;
     }
 
+    // Called by Session - does not need to check uid
+    UserData getUserData() {
+        synchronized (mLock) {
+            return mUserData;
+        }
+    }
+
+    // Called by AutofillManager
+    UserData getUserData(int callingUid) {
+        synchronized (mLock) {
+            if (isCalledByServiceLocked("getUserData", callingUid)) {
+                return mUserData;
+            }
+        }
+        return null;
+    }
+
+    // Called by AutofillManager
+    void setUserData(int callingUid, UserData userData) {
+        synchronized (mLock) {
+            if (isCalledByServiceLocked("setUserData", callingUid)) {
+                mUserData = userData;
+            }
+        }
+    }
+
+    private boolean isCalledByServiceLocked(String methodName, int callingUid) {
+        if (getServiceUidLocked() != callingUid) {
+            Slog.w(TAG, methodName + "() called by UID " + callingUid
+                    + ", but service UID is " + getServiceUidLocked());
+            return false;
+        }
+        return true;
+    }
+
     void dumpLocked(String prefix, PrintWriter pw) {
         final String prefix2 = prefix + "  ";
 
         pw.print(prefix); pw.print("User: "); pw.println(mUserId);
+        pw.print(prefix); pw.print("UID: "); pw.println(getServiceUidLocked());
         pw.print(prefix); pw.print("Component: "); pw.println(mInfo != null
                 ? mInfo.getServiceInfo().getComponentName() : null);
         pw.print(prefix); pw.print("Component from settings: ");
@@ -762,8 +812,13 @@
             }
         }
 
-        pw.print(prefix); pw.println("Clients");
-        mClients.dump(pw, prefix2);
+        pw.print(prefix); pw.print("Clients: ");
+        if (mClients == null) {
+            pw.println("N/A");
+        } else {
+            pw.println();
+            mClients.dump(pw, prefix2);
+        }
 
         if (mEventHistory == null || mEventHistory.getEvents() == null
                 || mEventHistory.getEvents().size() == 0) {
@@ -779,6 +834,14 @@
                         + event.getDatasetId());
             }
         }
+
+        pw.print(prefix); pw.print("User data: ");
+        if (mUserData == null) {
+            pw.println("N/A");
+        } else {
+            pw.println();
+            mUserData.dump(prefix2, pw);
+        }
     }
 
     void destroySessionsLocked() {
diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
index 831c488..aea9ad0 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
@@ -97,7 +97,7 @@
     private PendingRequest mPendingRequest;
 
     public interface FillServiceCallbacks {
-        void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response, int serviceUid,
+        void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response,
                 @NonNull String servicePackageName);
         void onFillRequestFailure(@Nullable CharSequence message,
                 @NonNull String servicePackageName);
@@ -281,11 +281,11 @@
         mContext.unbindService(mServiceConnection);
     }
 
-    private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest,
-            int callingUid, int requestFlags, FillResponse response) {
+    private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest, int requestFlags,
+            FillResponse response) {
         mHandler.getHandler().post(() -> {
             if (handleResponseCallbackCommon(pendingRequest)) {
-                mCallbacks.onFillRequestSuccess(requestFlags, response, callingUid,
+                mCallbacks.onFillRequestSuccess(requestFlags, response,
                         mComponentName.getPackageName());
             }
         });
@@ -546,7 +546,7 @@
                     final RemoteFillService remoteService = getService();
                     if (remoteService != null) {
                         remoteService.dispatchOnFillRequestSuccess(PendingFillRequest.this,
-                                getCallingUid(), request.getFlags(), response);
+                                request.getFlags(), response);
                     }
                 }
 
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index e3db1b1..3615bca 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -55,7 +55,6 @@
 import android.os.SystemClock;
 import android.service.autofill.AutofillService;
 import android.service.autofill.Dataset;
-import android.service.autofill.FieldsDetection;
 import android.service.autofill.FillContext;
 import android.service.autofill.FillRequest;
 import android.service.autofill.FillResponse;
@@ -63,6 +62,7 @@
 import android.service.autofill.InternalValidator;
 import android.service.autofill.SaveInfo;
 import android.service.autofill.SaveRequest;
+import android.service.autofill.UserData;
 import android.service.autofill.ValueFinder;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -485,7 +485,7 @@
     // FillServiceCallbacks
     @Override
     public void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response,
-            int serviceUid, @NonNull String servicePackageName) {
+            @NonNull String servicePackageName) {
         synchronized (mLock) {
             if (mDestroyed) {
                 Slog.w(TAG, "Call to Session#onFillRequestSuccess() rejected - session: "
@@ -499,13 +499,13 @@
         }
 
         // TODO(b/67867469): remove once feature is finished
-        if (response.getFieldsDetection() != null && !mService.isFieldDetectionEnabled()) {
+        if (response.getFieldClassificationIds() != null && !mService.isFieldDetectionEnabled()) {
             Slog.w(TAG, "Ignoring " + response + " because field detection is disabled");
             processNullResponseLocked(requestFlags);
             return;
         }
 
-        mService.setLastResponse(serviceUid, id, response);
+        mService.setLastResponse(id, response);
 
         int sessionFinishedState = 0;
         final long disableDuration = response.getDisableDuration();
@@ -908,7 +908,7 @@
             final FillResponse response = mResponses.valueAt(i);
             final List<Dataset> datasets = response.getDatasets();
             if (datasets == null || datasets.isEmpty()) {
-                if (sVerbose) Slog.v(TAG,  "logContextCommitted() no datasets at " + i);
+                if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i);
             } else {
                 for (int j = 0; j < datasets.size(); j++) {
                     final Dataset dataset = datasets.get(j);
@@ -931,25 +931,27 @@
                 }
             }
         }
-        final FieldsDetection fieldsDetection = lastResponse.getFieldsDetection();
+        final AutofillId[] fieldClassificationIds = lastResponse.getFieldClassificationIds();
 
-        if (!hasAtLeastOneDataset && fieldsDetection == null) {
+        if (!hasAtLeastOneDataset && fieldClassificationIds == null) {
             if (sVerbose) {
                 Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets nor fields "
-                        + "detection)");
+                        + "classification ids)");
             }
             return;
         }
 
+        final UserData userData = mService.getUserData();
         final AutofillId detectableFieldId;
         final String detectableRemoteId;
         String detectedRemoteId = null;
-        if (fieldsDetection == null) {
+        if (userData == null) {
             detectableFieldId = null;
             detectableRemoteId = null;
         } else {
-            detectableFieldId = fieldsDetection.getFieldId();
-            detectableRemoteId = fieldsDetection.getRemoteId();
+            // TODO(b/67867469): hardcoded to just first entry on initial refactoring.
+            detectableFieldId = fieldClassificationIds[0];
+            detectableRemoteId = userData.getRemoteIds()[0];
         }
 
         int detectedFieldScore = -1;
@@ -1062,7 +1064,8 @@
                     if (detectableFieldId != null && detectableFieldId.equals(viewState.id)
                             && currentValue.isText() && currentValue.getTextValue() != null) {
                         final String actualValue = currentValue.getTextValue().toString();
-                        final String expectedValue = fieldsDetection.getValue();
+                        // TODO(b/67867469): hardcoded to just first entry on initial refactoring.
+                        final String expectedValue = userData.getValues()[0];
                         if (actualValue.equalsIgnoreCase(expectedValue)) {
                             detectedRemoteId = detectableRemoteId;
                             detectedFieldScore = 0;
diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags
index d3ce306..6174aec 100644
--- a/services/core/java/com/android/server/EventLogTags.logtags
+++ b/services/core/java/com/android/server/EventLogTags.logtags
@@ -32,9 +32,11 @@
 # The device is being asked to go into a soft sleep (typically by the ungaze gesture).
 # It logs the time remaining before the device would've normally gone to sleep without the request.
 2731 power_soft_sleep_requested (savedwaketimems|2)
+# Power save state has changed. See BatterySaverController.java for the details.
+2739 battery_saver_mode (prevOffOrOn|1|5),(nowOffOrOn|1|5),(interactive|1|5),(features|3|5)
 
 #
-# Leave IDs through 2739 for more power logs (2730 used by battery_discharge above)
+# Leave IDs through 2740 for more power logs (2730 used by battery_discharge above)
 #
 
 
diff --git a/services/core/java/com/android/server/notification/ScheduleCalendar.java b/services/core/java/com/android/server/notification/ScheduleCalendar.java
index 40230bd..5ff0e21 100644
--- a/services/core/java/com/android/server/notification/ScheduleCalendar.java
+++ b/services/core/java/com/android/server/notification/ScheduleCalendar.java
@@ -18,6 +18,7 @@
 
 import android.service.notification.ZenModeConfig.ScheduleInfo;
 import android.util.ArraySet;
+import android.util.Log;
 
 import java.util.Calendar;
 import java.util.Objects;
@@ -41,10 +42,25 @@
     }
 
     public void maybeSetNextAlarm(long now, long nextAlarm) {
-        if (mSchedule != null) {
-            if (mSchedule.exitAtAlarm
-                    && (now > mSchedule.nextAlarm || nextAlarm < mSchedule.nextAlarm)) {
-                mSchedule.nextAlarm = nextAlarm;
+        if (mSchedule != null && mSchedule.exitAtAlarm) {
+            // alarm canceled
+            if (nextAlarm == 0) {
+                mSchedule.nextAlarm = 0;
+            }
+            // only allow alarms in the future
+            if (nextAlarm > now) {
+                // store earliest alarm
+                if (mSchedule.nextAlarm == 0) {
+                    mSchedule.nextAlarm = nextAlarm;
+                } else {
+                    mSchedule.nextAlarm = Math.min(mSchedule.nextAlarm, nextAlarm);
+                }
+            } else if (mSchedule.nextAlarm < now) {
+                if (ScheduleConditionProvider.DEBUG) {
+                    Log.d(ScheduleConditionProvider.TAG,
+                            "All alarms are in the past " + mSchedule.nextAlarm);
+                }
+                mSchedule.nextAlarm = 0;
             }
         }
     }
@@ -87,6 +103,9 @@
     }
 
     public boolean shouldExitForAlarm(long time) {
+        if (mSchedule == null) {
+            return false;
+        }
         return mSchedule.exitAtAlarm
                 && mSchedule.nextAlarm != 0
                 && time >= mSchedule.nextAlarm;
diff --git a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
index 50a51b2..c5f80bb 100644
--- a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
+++ b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
@@ -37,6 +37,8 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.notification.NotificationManagerService.DumpFilter;
 
 import java.io.PrintWriter;
@@ -62,10 +64,9 @@
     private static final String SEPARATOR = ";";
     private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
 
-
     private final Context mContext = this;
     private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
-    private ArraySet<Uri> mSnoozed = new ArraySet<>();
+    private ArraySet<Uri> mSnoozedForAlarm = new ArraySet<>();
 
     private AlarmManager mAlarmManager;
     private boolean mConnected;
@@ -102,7 +103,7 @@
                 pw.println(mSubscriptions.get(conditionId).toString());
             }
         }
-        pw.println("      snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozed));
+        pw.println("      snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozedForAlarm));
         dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, now);
     }
 
@@ -129,7 +130,7 @@
     public void onSubscribe(Uri conditionId) {
         if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
         if (!ZenModeConfig.isValidScheduleConditionId(conditionId)) {
-            notifyCondition(createCondition(conditionId, Condition.STATE_FALSE, "badCondition"));
+            notifyCondition(createCondition(conditionId, Condition.STATE_ERROR, "invalidId"));
             return;
         }
         synchronized (mSubscriptions) {
@@ -169,32 +170,11 @@
         synchronized (mSubscriptions) {
             setRegistered(!mSubscriptions.isEmpty());
             for (Uri conditionId : mSubscriptions.keySet()) {
-                final ScheduleCalendar cal = mSubscriptions.get(conditionId);
-                if (cal != null && cal.isInSchedule(now)) {
-                    if (conditionSnoozed(conditionId) || cal.shouldExitForAlarm(now)) {
-                        conditionsToNotify.add(createCondition(
-                                conditionId, Condition.STATE_FALSE, "alarmCanceled"));
-                        addSnoozed(conditionId);
-                    } else {
-                        conditionsToNotify.add(createCondition(
-                                conditionId, Condition.STATE_TRUE, "meetsSchedule"));
-                    }
-                    cal.maybeSetNextAlarm(now, nextUserAlarmTime);
-                } else {
-                    conditionsToNotify.add(createCondition(
-                            conditionId, Condition.STATE_FALSE, "!meetsSchedule"));
-                    removeSnoozed(conditionId);
-                    if (cal != null && nextUserAlarmTime == 0) {
-                        cal.maybeSetNextAlarm(now, nextUserAlarmTime);
-                    }
-                }
-                if (cal != null) {
-                    final long nextChangeTime = cal.getNextChangeTime(now);
-                    if (nextChangeTime > 0 && nextChangeTime > now) {
-                        if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
-                            mNextAlarmTime = nextChangeTime;
-                        }
-                    }
+                Condition condition =
+                        evaluateSubscriptionLocked(conditionId, mSubscriptions.get(conditionId),
+                                now, nextUserAlarmTime);
+                if (condition != null) {
+                    conditionsToNotify.add(condition);
                 }
             }
         }
@@ -202,6 +182,39 @@
         updateAlarm(now, mNextAlarmTime);
     }
 
+    @VisibleForTesting
+    @GuardedBy("mSubscriptions")
+    Condition evaluateSubscriptionLocked(Uri conditionId, ScheduleCalendar cal,
+            long now, long nextUserAlarmTime) {
+        Condition condition;
+        if (cal == null) {
+            condition = createCondition(conditionId, Condition.STATE_ERROR, "!invalidId");
+            removeSnoozed(conditionId);
+            return condition;
+        }
+        if (cal.isInSchedule(now)) {
+            if (conditionSnoozed(conditionId)) {
+                condition = createCondition(conditionId, Condition.STATE_FALSE, "snoozed");
+            } else if (cal.shouldExitForAlarm(now)) {
+                condition = createCondition(conditionId, Condition.STATE_FALSE, "alarmCanceled");
+                addSnoozed(conditionId);
+            } else {
+                condition = createCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
+            }
+        } else {
+            condition = createCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
+            removeSnoozed(conditionId);
+        }
+        cal.maybeSetNextAlarm(now, nextUserAlarmTime);
+        final long nextChangeTime = cal.getNextChangeTime(now);
+        if (nextChangeTime > 0 && nextChangeTime > now) {
+            if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
+                mNextAlarmTime = nextChangeTime;
+            }
+        }
+        return condition;
+    }
+
     private void updateAlarm(long now, long time) {
         final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
         final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
@@ -266,27 +279,28 @@
     }
 
     private boolean conditionSnoozed(Uri conditionId) {
-        synchronized (mSnoozed) {
-            return mSnoozed.contains(conditionId);
+        synchronized (mSnoozedForAlarm) {
+            return mSnoozedForAlarm.contains(conditionId);
         }
     }
 
-    private void addSnoozed(Uri conditionId) {
-        synchronized (mSnoozed) {
-            mSnoozed.add(conditionId);
+    @VisibleForTesting
+    void addSnoozed(Uri conditionId) {
+        synchronized (mSnoozedForAlarm) {
+            mSnoozedForAlarm.add(conditionId);
             saveSnoozedLocked();
         }
     }
 
     private void removeSnoozed(Uri conditionId) {
-        synchronized (mSnoozed) {
-            mSnoozed.remove(conditionId);
+        synchronized (mSnoozedForAlarm) {
+            mSnoozedForAlarm.remove(conditionId);
             saveSnoozedLocked();
         }
     }
 
-    public void saveSnoozedLocked() {
-        final String setting = TextUtils.join(SEPARATOR, mSnoozed);
+    private void saveSnoozedLocked() {
+        final String setting = TextUtils.join(SEPARATOR, mSnoozedForAlarm);
         final int currentUser = ActivityManager.getCurrentUser();
         Settings.Secure.putStringForUser(mContext.getContentResolver(),
                 SCP_SETTING,
@@ -294,8 +308,8 @@
                 currentUser);
     }
 
-    public void readSnoozed() {
-        synchronized (mSnoozed) {
+    private void readSnoozed() {
+        synchronized (mSnoozedForAlarm) {
             long identity = Binder.clearCallingIdentity();
             try {
                 final String setting = Settings.Secure.getStringForUser(
@@ -312,7 +326,7 @@
                         if (TextUtils.isEmpty(token)) {
                             continue;
                         }
-                        mSnoozed.add(Uri.parse(token));
+                        mSnoozedForAlarm.add(Uri.parse(token));
                     }
                 }
             } finally {
diff --git a/services/core/java/com/android/server/power/BatterySaverPolicy.java b/services/core/java/com/android/server/power/BatterySaverPolicy.java
index 3ccf17e..3810192 100644
--- a/services/core/java/com/android/server/power/BatterySaverPolicy.java
+++ b/services/core/java/com/android/server/power/BatterySaverPolicy.java
@@ -87,6 +87,12 @@
     private String mDeviceSpecificSettingsSource; // For dump() only.
 
     /**
+     * A short string describing which battery saver is now enabled, which we dump in the eventlog.
+     */
+    @GuardedBy("mLock")
+    private String mEventLogKeys;
+
+    /**
      * {@code true} if vibration is disabled in battery saver mode.
      *
      * @see Settings.Global#BATTERY_SAVER_CONSTANTS
@@ -354,6 +360,27 @@
 
         mFilesForNoninteractive = (new CpuFrequencies()).parseString(
                 parser.getString(KEY_CPU_FREQ_NONINTERACTIVE, "")).toSysFileMap();
+
+        final StringBuilder sb = new StringBuilder();
+
+        if (mForceAllAppsStandby) sb.append("A");
+        if (mForceBackgroundCheck) sb.append("B");
+
+        if (mVibrationDisabled) sb.append("v");
+        if (mAnimationDisabled) sb.append("a");
+        if (mSoundTriggerDisabled) sb.append("s");
+        if (mFullBackupDeferred) sb.append("F");
+        if (mKeyValueBackupDeferred) sb.append("K");
+        if (!mFireWallDisabled) sb.append("f");
+        if (!mDataSaverDisabled) sb.append("d");
+        if (!mAdjustBrightnessDisabled) sb.append("b");
+
+        if (mLaunchBoostDisabled) sb.append("l");
+        if (mOptionalSensorsDisabled) sb.append("S");
+
+        sb.append(mGpsMode);
+
+        mEventLogKeys = sb.toString();
     }
 
     /**
@@ -431,6 +458,12 @@
         }
     }
 
+    public String toEventLogString() {
+        synchronized (mLock) {
+            return mEventLogKeys;
+        }
+    }
+
     public void dump(PrintWriter pw) {
         synchronized (mLock) {
             pw.println();
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
index 1b19c33..a6bca0b 100644
--- a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
@@ -45,6 +45,7 @@
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
+import com.android.server.EventLogTags;
 import com.android.server.LocalServices;
 import com.android.server.power.BatterySaverPolicy;
 import com.android.server.power.BatterySaverPolicy.BatterySaverPolicyListener;
@@ -77,6 +78,12 @@
     @GuardedBy("mLock")
     private boolean mEnabled;
 
+    /**
+     * Previously enabled or not; only for the event logging. Only use it from
+     * {@link #handleBatterySaverStateChanged}.
+     */
+    private boolean mPreviouslyEnabled;
+
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -213,12 +220,18 @@
         final ArrayMap<String, String> fileValues;
 
         synchronized (mLock) {
-            Slog.i(TAG, "Battery saver " + (mEnabled ? "enabled" : "disabled")
-                    + ": isInteractive=" + isInteractive);
+            EventLogTags.writeBatterySaverMode(
+                    mPreviouslyEnabled ? 1 : 0, // Previously off or on.
+                    mEnabled ? 1 : 0, // Now off or on.
+                    isInteractive ?  1 : 0, // Device interactive state.
+                    mEnabled ? mBatterySaverPolicy.toEventLogString() : "");
+            mPreviouslyEnabled = mEnabled;
 
             listeners = mListeners.toArray(new LowPowerModeListener[mListeners.size()]);
+
             enabled = mEnabled;
 
+
             if (enabled) {
                 fileValues = mBatterySaverPolicy.getFileValues(isInteractive);
             } else {
diff --git a/services/tests/notification/src/com/android/server/notification/ScheduleCalendarTest.java b/services/tests/notification/src/com/android/server/notification/ScheduleCalendarTest.java
new file mode 100644
index 0000000..cbda12d
--- /dev/null
+++ b/services/tests/notification/src/com/android/server/notification/ScheduleCalendarTest.java
@@ -0,0 +1,327 @@
+/*
+ * 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 com.android.server.notification;
+
+import static junit.framework.Assert.assertFalse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.service.notification.ZenModeConfig;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Slog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ScheduleCalendarTest extends NotificationTestCase {
+
+    private ScheduleCalendar mScheduleCalendar;
+    private ZenModeConfig.ScheduleInfo mScheduleInfo;
+
+    @Before
+    public void setUp() throws Exception {
+        mScheduleCalendar = new ScheduleCalendar();
+        mScheduleInfo = new ZenModeConfig.ScheduleInfo();
+        mScheduleInfo.days = new int[] {1, 2, 3, 4, 5};
+        mScheduleCalendar.setSchedule(mScheduleInfo);
+    }
+
+    @Test
+    public void testNullScheduleInfo() throws Exception {
+        mScheduleCalendar.setSchedule(null);
+
+        mScheduleCalendar.maybeSetNextAlarm(1000, 1999);
+        assertEquals(0, mScheduleCalendar.getNextChangeTime(1000));
+        assertFalse(mScheduleCalendar.isInSchedule(100));
+        assertFalse(mScheduleCalendar.shouldExitForAlarm(100));
+    }
+
+    @Test
+    public void testGetNextChangeTime_startToday() throws Exception {
+        Calendar cal = new GregorianCalendar();
+        cal.set(Calendar.HOUR_OF_DAY, 1);
+        cal.set(Calendar.MINUTE, 15);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        mScheduleInfo.days = new int[] {getTodayDay()};
+        mScheduleInfo.startHour = cal.get(Calendar.HOUR_OF_DAY) + 1;
+        mScheduleInfo.endHour = cal.get(Calendar.HOUR_OF_DAY) + 3;
+        mScheduleInfo.startMinute = 15;
+        mScheduleInfo.endMinute = 15;
+        mScheduleInfo.exitAtAlarm = false;
+        mScheduleCalendar.setSchedule(mScheduleInfo);
+
+        Calendar expected = new GregorianCalendar();
+        expected.setTimeInMillis(cal.getTimeInMillis());
+        expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.startHour);
+
+        long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+        GregorianCalendar actual = new GregorianCalendar();
+        actual.setTimeInMillis(actualMs);
+        assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+                actualMs);
+    }
+
+    @Test
+    public void testGetNextChangeTime_endToday() throws Exception {
+        Calendar cal = new GregorianCalendar();
+        cal.set(Calendar.HOUR_OF_DAY, 2);
+        cal.set(Calendar.MINUTE, 15);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        mScheduleInfo.days = new int[] {getTodayDay()};
+        mScheduleInfo.startHour = cal.get(Calendar.HOUR_OF_DAY) - 1;
+        mScheduleInfo.endHour = cal.get(Calendar.HOUR_OF_DAY) + 3;
+        mScheduleInfo.startMinute = 15;
+        mScheduleInfo.endMinute = 15;
+        mScheduleInfo.exitAtAlarm = false;
+
+        Calendar expected = new GregorianCalendar();
+        expected.setTimeInMillis(cal.getTimeInMillis());
+        expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.endHour);
+        expected.set(Calendar.MINUTE, mScheduleInfo.endMinute);
+
+        long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+        GregorianCalendar actual = new GregorianCalendar();
+        actual.setTimeInMillis(actualMs);
+        assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+                actualMs);
+    }
+
+    @Test
+    public void testGetNextChangeTime_startTomorrow() throws Exception {
+        Calendar cal = new GregorianCalendar();
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 15);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        mScheduleInfo.days = new int[] {getTodayDay(), getTodayDay() + 1};
+        mScheduleInfo.startHour = 1;
+        mScheduleInfo.endHour = 3;
+        mScheduleInfo.startMinute = 15;
+        mScheduleInfo.endMinute = 15;
+        mScheduleInfo.exitAtAlarm = false;
+
+        Calendar expected = new GregorianCalendar();
+        expected.setTimeInMillis(cal.getTimeInMillis());
+        expected.add(Calendar.DATE, 1);
+        expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.startHour);
+        expected.set(Calendar.MINUTE, mScheduleInfo.startMinute);
+
+        long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+        GregorianCalendar actual = new GregorianCalendar();
+        actual.setTimeInMillis(actualMs);
+        assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+                actualMs);
+    }
+
+    @Test
+    public void testGetNextChangeTime_endTomorrow() throws Exception {
+        Calendar cal = new GregorianCalendar();
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 15);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        mScheduleInfo.days = new int[] {getTodayDay(), getTodayDay() + 1};
+        mScheduleInfo.startHour = 22;
+        mScheduleInfo.endHour = 3;
+        mScheduleInfo.startMinute = 15;
+        mScheduleInfo.endMinute = 15;
+        mScheduleInfo.exitAtAlarm = false;
+
+        Calendar expected = new GregorianCalendar();
+        expected.setTimeInMillis(cal.getTimeInMillis());
+        expected.add(Calendar.DATE, 1);
+        expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.endHour);
+        expected.set(Calendar.MINUTE, mScheduleInfo.endMinute);
+
+        long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+        GregorianCalendar actual = new GregorianCalendar();
+        actual.setTimeInMillis(actualMs);
+        assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+                actualMs);
+    }
+
+    @Test
+    public void testShouldExitForAlarm_settingOff() {
+        mScheduleInfo.exitAtAlarm = false;
+        mScheduleInfo.nextAlarm = 1000;
+
+        assertFalse(mScheduleCalendar.shouldExitForAlarm(1000));
+    }
+
+    @Test
+    public void testShouldExitForAlarm_beforeAlarm() {
+        mScheduleInfo.exitAtAlarm = true;
+        mScheduleInfo.nextAlarm = 1000;
+
+        assertFalse(mScheduleCalendar.shouldExitForAlarm(999));
+    }
+
+    @Test
+    public void testShouldExitForAlarm_noAlarm() {
+        mScheduleInfo.exitAtAlarm = true;
+        mScheduleInfo.nextAlarm = 0;
+
+        assertFalse(mScheduleCalendar.shouldExitForAlarm(999));
+    }
+
+    @Test
+    public void testShouldExitForAlarm() {
+        mScheduleInfo.exitAtAlarm = true;
+        mScheduleInfo.nextAlarm = 1000;
+
+        assertTrue(mScheduleCalendar.shouldExitForAlarm(1000));
+    }
+
+    @Test
+    public void testMaybeSetNextAlarm_settingOff() {
+        mScheduleInfo.exitAtAlarm = false;
+        mScheduleInfo.nextAlarm = 0;
+
+        mScheduleCalendar.maybeSetNextAlarm(1000, 2000);
+
+        assertEquals(0, mScheduleInfo.nextAlarm);
+    }
+
+    @Test
+    public void testMaybeSetNextAlarm_settingOn() {
+        mScheduleInfo.exitAtAlarm = true;
+        mScheduleInfo.nextAlarm = 0;
+
+        mScheduleCalendar.maybeSetNextAlarm(1000, 2000);
+
+        assertEquals(2000, mScheduleInfo.nextAlarm);
+    }
+
+    @Test
+    public void testMaybeSetNextAlarm_alarmCanceled() {
+        mScheduleInfo.exitAtAlarm = true;
+        mScheduleInfo.nextAlarm = 10000;
+
+        mScheduleCalendar.maybeSetNextAlarm(1000, 0);
+
+        assertEquals(0, mScheduleInfo.nextAlarm);
+    }
+
+    @Test
+    public void testMaybeSetNextAlarm_earlierAlarm() {
+        mScheduleInfo.exitAtAlarm = true;
+        mScheduleInfo.nextAlarm = 2000;
+
+        mScheduleCalendar.maybeSetNextAlarm(1000, 1500);
+
+        assertEquals(1500, mScheduleInfo.nextAlarm);
+    }
+
+    @Test
+    public void testMaybeSetNextAlarm_laterAlarm() {
+        mScheduleInfo.exitAtAlarm = true;
+        mScheduleInfo.nextAlarm = 2000;
+
+        mScheduleCalendar.maybeSetNextAlarm(1000, 3000);
+
+        assertEquals(2000, mScheduleInfo.nextAlarm);
+    }
+
+    @Test
+    public void testMaybeSetNextAlarm_expiredAlarm() {
+        mScheduleInfo.exitAtAlarm = true;
+        mScheduleInfo.nextAlarm = 998;
+
+        mScheduleCalendar.maybeSetNextAlarm(1000, 999);
+
+        assertEquals(0, mScheduleInfo.nextAlarm);
+    }
+
+    @Test
+    public void testIsInSchedule_inScheduleOvernight() {
+        Calendar cal = new GregorianCalendar();
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 15);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        mScheduleInfo.days = new int[] {getTodayDay()};
+        mScheduleInfo.startHour = 22;
+        mScheduleInfo.endHour = 3;
+        mScheduleInfo.startMinute = 15;
+        mScheduleInfo.endMinute = 15;
+
+        assertTrue(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+    }
+
+    @Test
+    public void testIsInSchedule_inScheduleSingleDay() {
+        Calendar cal = new GregorianCalendar();
+        cal.set(Calendar.HOUR_OF_DAY, 14);
+        cal.set(Calendar.MINUTE, 15);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        mScheduleInfo.days = new int[] {getTodayDay()};
+        mScheduleInfo.startHour = 12;
+        mScheduleInfo.endHour = 3;
+        mScheduleInfo.startMinute = 16;
+        mScheduleInfo.endMinute = 15;
+
+        assertTrue(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+    }
+
+    @Test
+    public void testIsInSchedule_notToday() {
+        Calendar cal = new GregorianCalendar();
+        cal.set(Calendar.HOUR_OF_DAY, 14);
+        cal.set(Calendar.MINUTE, 15);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        cal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
+        mScheduleInfo.days = new int[] {Calendar.FRIDAY, Calendar.SUNDAY};
+        mScheduleInfo.startHour = 12;
+        mScheduleInfo.startMinute = 16;
+        mScheduleInfo.endHour = 15;
+        mScheduleInfo.endMinute = 15;
+
+        assertFalse(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+    }
+
+    @Test
+    public void testIsInSchedule_startingSoon() {
+        Calendar cal = new GregorianCalendar();
+        cal.set(Calendar.HOUR_OF_DAY, 14);
+        cal.set(Calendar.MINUTE, 15);
+        cal.set(Calendar.SECOND, 59);
+        cal.set(Calendar.MILLISECOND, 0);
+        mScheduleInfo.days = new int[] {getTodayDay()};
+        mScheduleInfo.startHour = 14;
+        mScheduleInfo.endHour = 3;
+        mScheduleInfo.startMinute = 16;
+        mScheduleInfo.endMinute = 15;
+
+        assertFalse(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+    }
+
+    private int getTodayDay() {
+        return new GregorianCalendar().get(Calendar.DAY_OF_WEEK);
+    }
+}
diff --git a/services/tests/notification/src/com/android/server/notification/ScheduleConditionProviderTest.java b/services/tests/notification/src/com/android/server/notification/ScheduleConditionProviderTest.java
new file mode 100644
index 0000000..ddf46a0
--- /dev/null
+++ b/services/tests/notification/src/com/android/server/notification/ScheduleConditionProviderTest.java
@@ -0,0 +1,337 @@
+package com.android.server.notification;
+
+import static org.mockito.Mockito.spy;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Looper;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.support.test.InstrumentationRegistry;
+import android.test.ServiceTestCase;
+import android.testing.TestableContext;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+public class ScheduleConditionProviderTest extends ServiceTestCase<ScheduleConditionProvider> {
+
+    ScheduleConditionProvider mService;
+
+    @Rule
+    public final TestableContext mContext =
+            new TestableContext(InstrumentationRegistry.getContext(), null);
+
+    public ScheduleConditionProviderTest() {
+        super(ScheduleConditionProvider.class);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        Looper.prepare();
+
+        Intent startIntent =
+                new Intent("com.android.server.notification.ScheduleConditionProvider");
+        startIntent.setPackage("android");
+        bindService(startIntent);
+        mService = spy(getService());
+   }
+
+    @Test
+    public void testIsValidConditionId_incomplete() throws Exception {
+        Uri badConditionId = Uri.EMPTY;
+        assertFalse(mService.isValidConditionId(badConditionId));
+        assertEquals(Condition.STATE_ERROR,
+                mService.evaluateSubscriptionLocked(badConditionId, null, 0, 1000).state);
+    }
+
+    @Test
+    public void testIsValidConditionId() throws Exception {
+        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+        info.days = new int[] {1, 2, 4};
+        info.startHour = 8;
+        info.startMinute = 56;
+        info.nextAlarm = 1000;
+        info.exitAtAlarm = true;
+        info.endHour = 12;
+        info.endMinute = 9;
+        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+        assertTrue(mService.isValidConditionId(conditionId));
+    }
+
+    @Test
+    public void testEvaluateSubscription_noAlarmExit_InSchedule() {
+        Calendar now = getNow();
+
+        // Schedule - 1 hour long; starts now
+        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+        info.days = new int[] {Calendar.FRIDAY};
+        info.startHour = now.get(Calendar.HOUR_OF_DAY);
+        info.startMinute = now.get(Calendar.MINUTE);
+        info.nextAlarm = 0;
+        info.exitAtAlarm = false;
+        info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+        info.endMinute = info.startMinute;
+        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+        ScheduleCalendar cal = new ScheduleCalendar();
+        cal.setSchedule(info);
+        assertTrue(cal.isInSchedule(now.getTimeInMillis()));
+
+        Condition condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+        assertEquals(Condition.STATE_TRUE, condition.state);
+    }
+
+    @Test
+    public void testEvaluateSubscription_noAlarmExit_InScheduleSnoozed() {
+        Calendar now = getNow();
+
+        // Schedule - 1 hour long; starts now
+        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+        info.days = new int[] {Calendar.FRIDAY};
+        info.startHour = now.get(Calendar.HOUR_OF_DAY);
+        info.startMinute = now.get(Calendar.MINUTE);
+        info.nextAlarm = 0;
+        info.exitAtAlarm = false;
+        info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+        info.endMinute = info.startMinute;
+        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+        ScheduleCalendar cal = new ScheduleCalendar();
+        cal.setSchedule(info);
+        assertTrue(cal.isInSchedule(now.getTimeInMillis()));
+
+        mService.addSnoozed(conditionId);
+
+        Condition condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+        assertEquals(Condition.STATE_FALSE, condition.state);
+    }
+
+    @Test
+    public void testEvaluateSubscription_noAlarmExit_beforeSchedule() {
+        Calendar now = new GregorianCalendar();
+        now.set(Calendar.HOUR_OF_DAY, 14);
+        now.set(Calendar.MINUTE, 15);
+        now.set(Calendar.SECOND, 59);
+        now.set(Calendar.MILLISECOND, 0);
+        now.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
+
+        // Schedule - 1 hour long; starts in 1 second
+        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+        info.days = new int[] {Calendar.FRIDAY};
+        info.startHour = now.get(Calendar.HOUR_OF_DAY);
+        info.startMinute = now.get(Calendar.MINUTE) + 1;
+        info.nextAlarm = 0;
+        info.exitAtAlarm = false;
+        info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+        info.endMinute = info.startMinute;
+        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+        ScheduleCalendar cal = new ScheduleCalendar();
+        cal.setSchedule(info);
+
+        Condition condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+        assertEquals(Condition.STATE_FALSE, condition.state);
+    }
+
+    @Test
+    public void testEvaluateSubscription_noAlarmExit_endSchedule() {
+        Calendar now = getNow();
+
+        // Schedule - 1 hour long; ends now
+        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+        info.days = new int[] {Calendar.FRIDAY};
+        info.startHour = now.get(Calendar.HOUR_OF_DAY) - 1;
+        info.startMinute = now.get(Calendar.MINUTE);
+        info.nextAlarm = 0;
+        info.exitAtAlarm = false;
+        info.endHour = now.get(Calendar.HOUR_OF_DAY);
+        info.endMinute = now.get(Calendar.MINUTE);
+        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+        ScheduleCalendar cal = new ScheduleCalendar();
+        cal.setSchedule(info);
+
+        Condition condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+        assertEquals(Condition.STATE_FALSE, condition.state);
+    }
+
+    @Test
+    public void testEvaluateSubscription_alarmSetBeforeInSchedule() {
+        Calendar now = getNow();
+
+        // Schedule - 1 hour long; starts now, ends with alarm
+        ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+        ScheduleCalendar cal = new ScheduleCalendar();
+        cal.setSchedule(info);
+
+        // an hour before start, update with an alarm that will fire during the schedule
+        mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis() - 1000, now.getTimeInMillis() + 1000);
+
+        // at start, should be in dnd
+        Condition condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+        assertEquals(Condition.STATE_TRUE, condition.state);
+
+        // at alarm fire time, should exit dnd
+        assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
+        assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+                cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
+        condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis() + 1000, 0);
+        assertEquals(Condition.STATE_FALSE, condition.state);
+    }
+
+    @Test
+    public void testEvaluateSubscription_alarmSetInSchedule() {
+        Calendar now = getNow();
+
+        // Schedule - 1 hour long; starts now, ends with alarm
+        ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+        ScheduleCalendar cal = new ScheduleCalendar();
+        cal.setSchedule(info);
+
+        // at start, should be in dnd
+        Condition condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis(), 0);
+        assertEquals(Condition.STATE_TRUE, condition.state);
+
+        // in schedule, update with alarm time, should be in dnd
+        condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis() + 500, now.getTimeInMillis() + 1000);
+        assertEquals(Condition.STATE_TRUE, condition.state);
+
+        // at alarm fire time, should exit dnd
+        assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
+        assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+                cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
+        condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis() + 1000, 0);
+        assertEquals(Condition.STATE_FALSE, condition.state);
+    }
+
+    @Test
+    public void testEvaluateSubscription_earlierAlarmSet() {
+        Calendar now = getNow();
+
+        // Schedule - 1 hour long; starts now, ends with alarm
+        ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+        ScheduleCalendar cal = new ScheduleCalendar();
+        cal.setSchedule(info);
+
+        // at start, should be in dnd, alarm in 2000 ms
+        Condition condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 2000);
+        assertEquals(Condition.STATE_TRUE, condition.state);
+
+        // in schedule, update with earlier alarm time, should be in dnd
+        condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis() + 500, now.getTimeInMillis() + 1000);
+        assertEquals(Condition.STATE_TRUE, condition.state);
+
+        // at earliest alarm fire time, should exit dnd
+        assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
+        assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+                cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
+        condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis() + 1000, 0);
+        assertEquals(Condition.STATE_FALSE, condition.state);
+    }
+
+    @Test
+    public void testEvaluateSubscription_laterAlarmSet() {
+        Calendar now = getNow();
+
+        // Schedule - 1 hour long; starts now, ends with alarm
+        ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+        ScheduleCalendar cal = new ScheduleCalendar();
+        cal.setSchedule(info);
+
+        // at start, should be in dnd, alarm in 500 ms
+        Condition condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 500);
+        assertEquals(Condition.STATE_TRUE, condition.state);
+
+        // in schedule, update with later alarm time, should be in dnd
+        condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis() + 250, now.getTimeInMillis() + 1000);
+        assertEquals(Condition.STATE_TRUE, condition.state);
+
+        // at earliest alarm fire time, should exit dnd
+        assertTrue(cal.isInSchedule(now.getTimeInMillis() + 500));
+        assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+                cal.shouldExitForAlarm(now.getTimeInMillis() + 500));
+        condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis() + 500, 0);
+        assertEquals(Condition.STATE_FALSE, condition.state);
+    }
+
+    @Test
+    public void testEvaluateSubscription_alarmCanceled() {
+        Calendar now = getNow();
+
+        // Schedule - 1 hour long; starts now, ends with alarm
+        ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+        Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+        ScheduleCalendar cal = new ScheduleCalendar();
+        cal.setSchedule(info);
+
+        // at start, should be in dnd, alarm in 500 ms
+        Condition condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 500);
+        assertEquals(Condition.STATE_TRUE, condition.state);
+
+        // in schedule, cancel alarm
+        condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis() + 250, 0);
+        assertEquals(Condition.STATE_TRUE, condition.state);
+
+        // at previous alarm time, should not exit DND
+        assertTrue(cal.isInSchedule(now.getTimeInMillis() + 500));
+        assertFalse(cal.shouldExitForAlarm(now.getTimeInMillis() + 500));
+        condition = mService.evaluateSubscriptionLocked(
+                conditionId, cal, now.getTimeInMillis() + 500, 0);
+        assertEquals(Condition.STATE_TRUE, condition.state);
+
+        // end of schedule, exit DND
+        now.add(Calendar.HOUR_OF_DAY, 1);
+        condition = mService.evaluateSubscriptionLocked(conditionId, cal, now.getTimeInMillis(), 0);
+        assertEquals(Condition.STATE_FALSE, condition.state);
+    }
+
+    private Calendar getNow() {
+        Calendar now = new GregorianCalendar();
+        now.set(Calendar.HOUR_OF_DAY, 14);
+        now.set(Calendar.MINUTE, 16);
+        now.set(Calendar.SECOND, 0);
+        now.set(Calendar.MILLISECOND, 0);
+        now.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
+        return now;
+    }
+
+    private ZenModeConfig.ScheduleInfo getScheduleEndsInHour(Calendar now) {
+        ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+        info.days = new int[] {Calendar.FRIDAY};
+        info.startHour = now.get(Calendar.HOUR_OF_DAY);
+        info.startMinute = now.get(Calendar.MINUTE);
+        info.exitAtAlarm = true;
+        info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+        info.endMinute = now.get(Calendar.MINUTE);
+        return info;
+    }
+}
diff --git a/wifi/java/android/net/wifi/rtt/ResponderConfig.java b/wifi/java/android/net/wifi/rtt/ResponderConfig.java
index b503769..1090bfa 100644
--- a/wifi/java/android/net/wifi/rtt/ResponderConfig.java
+++ b/wifi/java/android/net/wifi/rtt/ResponderConfig.java
@@ -248,7 +248,10 @@
      * Point (AP), which can be obtained from {@link android.net.wifi.WifiManager#getScanResults()}.
      */
     public static ResponderConfig fromScanResult(ScanResult scanResult) {
-        byte[] macAddress = new MacAddress(scanResult.BSSID).toByteArray();
+        byte[] macAddress = null;
+        if (scanResult.BSSID != null) {
+            macAddress = MacAddress.byteAddrFromStringAddr(scanResult.BSSID);
+        }
         int responderType = RESPONDER_AP;
         boolean supports80211mc = scanResult.is80211mcResponder();
         int channelWidth = translcateScanResultChannelWidth(scanResult.channelWidth);