Merge "Pass in event time when notifying listeners of a state change"
diff --git a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java
index 8019d4f..4c44334 100644
--- a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java
+++ b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java
@@ -17,13 +17,10 @@
 package android.os;
 
 import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
 import android.content.Context;
 
-import java.util.List;
-
 /**
  * Access to the service that keeps track of device idleness and drives low power mode based on
  * that.
@@ -75,21 +72,6 @@
     }
 
     /**
-     * Add the specified packages to the power save whitelist.
-     *
-     * @return the number of packages that were successfully added to the whitelist
-     */
-    @RequiresPermission(android.Manifest.permission.DEVICE_POWER)
-    public int addPowerSaveWhitelistApps(@NonNull List<String> packageNames) {
-        try {
-            return mService.addPowerSaveWhitelistApps(packageNames);
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
-            return 0;
-        }
-    }
-
-    /**
      * Return whether a given package is in the power-save whitelist or not.
      * @hide
      */
diff --git a/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
index 7a3ed92..4ffcf8a 100644
--- a/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
+++ b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
@@ -26,6 +26,8 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * Interface to access and modify the power save whitelist.
@@ -78,6 +80,31 @@
     }
 
     /**
+     * Add the specified package to the power save whitelist.
+     *
+     * @return true if the package was successfully added to the whitelist
+     */
+    @RequiresPermission(android.Manifest.permission.DEVICE_POWER)
+    public boolean addToWhitelist(@NonNull String packageName) {
+        return addToWhitelist(Collections.singletonList(packageName)) == 1;
+    }
+
+    /**
+     * Add the specified packages to the power save whitelist.
+     *
+     * @return the number of packages that were successfully added to the whitelist
+     */
+    @RequiresPermission(android.Manifest.permission.DEVICE_POWER)
+    public int addToWhitelist(@NonNull List<String> packageNames) {
+        try {
+            return mService.addPowerSaveWhitelistApps(packageNames);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+            return 0;
+        }
+    }
+
+    /**
      * Add an app to the temporary whitelist for a short amount of time.
      *
      * @param packageName The package to add to the temp whitelist
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index bcd8be7..2f8b513 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -1171,7 +1171,8 @@
     private void fetchCarrierPrivilegedAppsLocked() {
         TelephonyManager telephonyManager =
                 mContext.getSystemService(TelephonyManager.class);
-        mCarrierPrivilegedApps = telephonyManager.getPackagesWithCarrierPrivilegesForAllPhones();
+        mCarrierPrivilegedApps =
+                telephonyManager.getCarrierPrivilegedPackagesForAllActiveSubscriptions();
         mHaveCarrierPrivilegedApps = true;
         if (DEBUG) {
             Slog.d(TAG, "apps with carrier privilege " + mCarrierPrivilegedApps);
diff --git a/apex/sdkext/Android.bp b/apex/sdkext/Android.bp
index b8dcb90..40f3c45 100644
--- a/apex/sdkext/Android.bp
+++ b/apex/sdkext/Android.bp
@@ -15,7 +15,13 @@
 apex {
     name: "com.android.sdkext",
     manifest: "manifest.json",
+    binaries: [ "derive_sdk" ],
     java_libs: [ "framework-sdkext" ],
+    prebuilts: [
+      "com.android.sdkext.ldconfig",
+      "cur_sdkinfo",
+      "derive_sdk.rc",
+    ],
     key: "com.android.sdkext.key",
     certificate: ":com.android.sdkext.certificate",
 }
@@ -30,3 +36,35 @@
     name: "com.android.sdkext.certificate",
     certificate: "com.android.sdkext",
 }
+
+prebuilt_etc {
+    name: "com.android.sdkext.ldconfig",
+    src: "ld.config.txt",
+    filename: "ld.config.txt",
+    installable: false,
+}
+
+python_binary_host {
+    name: "gen_sdkinfo",
+    srcs: [
+        "derive_sdk/sdk.proto",
+        "gen_sdkinfo.py",
+    ],
+    proto: {
+        canonical_path_from_root: false,
+    },
+}
+
+gensrcs {
+    name: "cur_sdkinfo_src",
+    srcs: [""],
+    tools: [ "gen_sdkinfo" ],
+    cmd: "$(location) -v 0 -o $(out)",
+}
+
+prebuilt_etc {
+    name: "cur_sdkinfo",
+    src: ":cur_sdkinfo_src",
+    filename: "sdkinfo.binarypb",
+    installable: false,
+}
diff --git a/apex/sdkext/derive_sdk/Android.bp b/apex/sdkext/derive_sdk/Android.bp
new file mode 100644
index 0000000..c4e3c29
--- /dev/null
+++ b/apex/sdkext/derive_sdk/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_binary {
+    name: "derive_sdk",
+    srcs: [
+        "derive_sdk.cpp",
+        "sdk.proto",
+    ],
+    proto: {
+        type: "lite",
+    },
+    sdk_version: "current",
+    stl: "c++_static",
+    shared_libs: [ "liblog" ],
+    static_libs: [
+        "libbase_ndk",
+        "libprotobuf-cpp-lite-ndk",
+    ],
+}
+
+prebuilt_etc {
+    name: "derive_sdk.rc",
+    src: "derive_sdk.rc",
+    installable: false,
+}
diff --git a/apex/sdkext/derive_sdk/derive_sdk.cpp b/apex/sdkext/derive_sdk/derive_sdk.cpp
new file mode 100644
index 0000000..0aacebe
--- /dev/null
+++ b/apex/sdkext/derive_sdk/derive_sdk.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "derive_sdk"
+
+#include <algorithm>
+#include <dirent.h>
+#include <iostream>
+#include <sys/stat.h>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+
+#include "frameworks/base/apex/sdkext/derive_sdk/sdk.pb.h"
+
+using com::android::sdkext::proto::SdkVersion;
+
+int main(int, char**) {
+    std::unique_ptr<DIR, decltype(&closedir)> apex(opendir("/apex"), closedir);
+    if (!apex) {
+        LOG(ERROR) << "Could not read /apex";
+        return EXIT_FAILURE;
+    }
+    struct dirent* de;
+    std::vector<std::string> paths;
+    while ((de = readdir(apex.get()))) {
+        std::string name = de->d_name;
+        if (name[0] == '.' || name.find('@') != std::string::npos) {
+            // Skip <name>@<ver> dirs, as they are bind-mounted to <name>
+            continue;
+        }
+        std::string path = "/apex/" + name + "/etc/sdkinfo.binarypb";
+        struct stat statbuf;
+        if (stat(path.c_str(), &statbuf) == 0) {
+            paths.push_back(path);
+        }
+    }
+
+    std::vector<int> versions;
+    for (const auto& path : paths) {
+        std::string contents;
+        if (!android::base::ReadFileToString(path, &contents, true)) {
+            LOG(ERROR) << "failed to read " << path;
+            continue;
+        }
+        SdkVersion sdk_version;
+        if (!sdk_version.ParseFromString(contents)) {
+            LOG(ERROR) << "failed to parse " << path;
+            continue;
+        }
+        versions.push_back(sdk_version.version());
+    }
+    auto itr = std::min_element(versions.begin(), versions.end());
+    std::string prop_value = itr == versions.end() ? "0" : std::to_string(*itr);
+
+    if (!android::base::SetProperty("persist.com.android.sdkext.sdk_info", prop_value)) {
+        LOG(ERROR) << "failed to set sdk_info prop";
+        return EXIT_FAILURE;
+    }
+
+    return EXIT_SUCCESS;
+}
diff --git a/apex/sdkext/derive_sdk/derive_sdk.rc b/apex/sdkext/derive_sdk/derive_sdk.rc
new file mode 100644
index 0000000..1b66794
--- /dev/null
+++ b/apex/sdkext/derive_sdk/derive_sdk.rc
@@ -0,0 +1,3 @@
+service derive_sdk /apex/com.android.sdkext/bin/derive_sdk
+    oneshot
+    disabled
diff --git a/apex/sdkext/derive_sdk/sdk.proto b/apex/sdkext/derive_sdk/sdk.proto
new file mode 100644
index 0000000..d15b935
--- /dev/null
+++ b/apex/sdkext/derive_sdk/sdk.proto
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+package com.android.sdkext.proto;
+
+option java_outer_classname = "SdkProto";
+option optimize_for = LITE_RUNTIME;
+
+message SdkVersion {
+  int32 version = 1;
+}
diff --git a/apex/sdkext/gen_sdkinfo.py b/apex/sdkext/gen_sdkinfo.py
new file mode 100644
index 0000000..5af478b
--- /dev/null
+++ b/apex/sdkext/gen_sdkinfo.py
@@ -0,0 +1,19 @@
+import sdk_pb2
+import sys
+
+if __name__ == '__main__':
+  argv = sys.argv[1:]
+  if not len(argv) == 4 or sorted([argv[0], argv[2]]) != ['-o', '-v']:
+    print('usage: gen_sdkinfo -v <version> -o <output-file>')
+    sys.exit(1)
+
+  for i in range(len(argv)):
+    if sys.argv[i] == '-o':
+      filename = sys.argv[i+1]
+    if sys.argv[i] == '-v':
+      version = int(sys.argv[i+1])
+
+  proto = sdk_pb2.SdkVersion()
+  proto.version = version
+  with open(filename, 'wb') as f:
+    f.write(proto.SerializeToString())
diff --git a/apex/sdkext/ld.config.txt b/apex/sdkext/ld.config.txt
new file mode 100644
index 0000000..b447068
--- /dev/null
+++ b/apex/sdkext/ld.config.txt
@@ -0,0 +1,31 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Bionic loader config file for the sdkext apex.
+
+dir.sdkext = /apex/com.android.sdkext/bin/
+
+[sdkext]
+additional.namespaces = platform
+
+namespace.default.isolated = true
+namespace.default.links = platform
+namespace.default.link.platform.allow_all_shared_libs = true
+
+###############################################################################
+# "platform" namespace: used for NDK libraries
+###############################################################################
+namespace.platform.isolated = true
+namespace.platform.search.paths = /system/${LIB}
+namespace.platform.asan.search.paths = /data/asan/system/${LIB}
+
+# /system/lib/libc.so, etc are symlinks to /apex/com.android.lib/lib/bionic/libc.so, etc.
+# Add /apex/... path to the permitted paths because linker uses realpath(3)
+# to check the accessibility of the lib. We could add this to search.paths
+# instead but that makes the resolution of bionic libs be dependent on
+# the order of /system/lib and /apex/... in search.paths. If /apex/...
+# is after /system/lib, then /apex/... is never tried because libc.so
+# is always found in /system/lib but fails to pass the accessibility test
+# because of its realpath.  It's better to not depend on the ordering if
+# possible.
+namespace.platform.permitted.paths = /apex/com.android.runtime/${LIB}/bionic
+namespace.platform.asan.permitted.paths = /apex/com.android.runtime/${LIB}/bionic
diff --git a/api/current.txt b/api/current.txt
index 8533ab1..eb506e8 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -29323,12 +29323,14 @@
     method public static long getMobileRxPackets();
     method public static long getMobileTxBytes();
     method public static long getMobileTxPackets();
+    method public static long getRxPackets(@NonNull String);
     method public static int getThreadStatsTag();
     method public static int getThreadStatsUid();
     method public static long getTotalRxBytes();
     method public static long getTotalRxPackets();
     method public static long getTotalTxBytes();
     method public static long getTotalTxPackets();
+    method public static long getTxPackets(@NonNull String);
     method public static long getUidRxBytes(int);
     method public static long getUidRxPackets(int);
     method @Deprecated public static long getUidTcpRxBytes(int);
@@ -43137,6 +43139,7 @@
     field public static final int MS_INVALIDATE;
     field public static final int MS_SYNC;
     field public static final int NETLINK_INET_DIAG;
+    field public static final int NETLINK_NETFILTER;
     field public static final int NETLINK_ROUTE;
     field public static final int NI_DGRAM;
     field public static final int NI_NAMEREQD;
diff --git a/api/system-current.txt b/api/system-current.txt
index fe9c0d1..c7bde93 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -6379,6 +6379,8 @@
   }
 
   public class PowerWhitelistManager {
+    method @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public boolean addToWhitelist(@NonNull String);
+    method @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public int addToWhitelist(@NonNull java.util.List<java.lang.String>);
     method @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST) public void whitelistAppTemporarily(@NonNull String, long);
     method @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST) public long whitelistAppTemporarilyForEvent(@NonNull String, int, @NonNull String);
     field public static final int EVENT_MMS = 2; // 0x2
@@ -9425,6 +9427,7 @@
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public android.telephony.ImsiEncryptionInfo getCarrierInfoForImsiEncryption(int);
     method public java.util.List<java.lang.String> getCarrierPackageNamesForIntent(android.content.Intent);
     method public java.util.List<java.lang.String> getCarrierPackageNamesForIntentAndPhone(android.content.Intent, int);
+    method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.List<java.lang.String> getCarrierPrivilegedPackagesForAllActiveSubscriptions();
     method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public android.telephony.CarrierRestrictionRules getCarrierRestrictionRules();
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public String getCdmaMdn();
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public String getCdmaMdn(int);
diff --git a/api/test-current.txt b/api/test-current.txt
index 3ddbbf8..3ce8c5e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1796,7 +1796,6 @@
   }
 
   public class DeviceIdleManager {
-    method @RequiresPermission("android.permission.DEVICE_POWER") public int addPowerSaveWhitelistApps(@NonNull java.util.List<java.lang.String>);
     method @NonNull public String[] getSystemPowerWhitelist();
     method @NonNull public String[] getSystemPowerWhitelistExceptIdle();
   }
@@ -2045,6 +2044,8 @@
   }
 
   public class PowerWhitelistManager {
+    method @RequiresPermission("android.permission.DEVICE_POWER") public boolean addToWhitelist(@NonNull String);
+    method @RequiresPermission("android.permission.DEVICE_POWER") public int addToWhitelist(@NonNull java.util.List<java.lang.String>);
     method @RequiresPermission("android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST") public void whitelistAppTemporarily(@NonNull String, long);
     method @RequiresPermission("android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST") public long whitelistAppTemporarilyForEvent(@NonNull String, int, @NonNull String);
     field public static final int EVENT_MMS = 2; // 0x2
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index e8c0e15..d10a661 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -1295,7 +1295,18 @@
                                     const sp<android::os::IPullAtomCallback>& pullerCallback) {
     ENFORCE_UID(AID_SYSTEM);
 
-    VLOG("StatsService::registerPuller called.");
+    VLOG("StatsService::registerPullAtomCallback called.");
+    mPullerManager->RegisterPullAtomCallback(uid, atomTag, coolDownNs, timeoutNs, additiveFields,
+                                             pullerCallback);
+    return Status::ok();
+}
+
+Status StatsService::registerNativePullAtomCallback(int32_t atomTag, int64_t coolDownNs,
+                                    int64_t timeoutNs, const std::vector<int32_t>& additiveFields,
+                                    const sp<android::os::IPullAtomCallback>& pullerCallback) {
+
+    VLOG("StatsService::registerNativePullAtomCallback called.");
+    int32_t uid = IPCThreadState::self()->getCallingUid();
     mPullerManager->RegisterPullAtomCallback(uid, atomTag, coolDownNs, timeoutNs, additiveFields,
                                              pullerCallback);
     return Status::ok();
diff --git a/cmds/statsd/src/StatsService.h b/cmds/statsd/src/StatsService.h
index 6d40007..8c98e7b 100644
--- a/cmds/statsd/src/StatsService.h
+++ b/cmds/statsd/src/StatsService.h
@@ -187,6 +187,13 @@
             const sp<android::os::IPullAtomCallback>& pullerCallback) override;
 
     /**
+     * Binder call to register a callback function for a pulled atom.
+     */
+    virtual Status registerNativePullAtomCallback(int32_t atomTag, int64_t coolDownNs,
+            int64_t timeoutNs, const std::vector<int32_t>& additiveFields,
+            const sp<android::os::IPullAtomCallback>& pullerCallback) override;
+
+    /**
      * Binder call to unregister any existing callback function for a vendor pulled atom.
      */
     virtual Status unregisterPullerCallback(int32_t atomTag, const String16& packageName) override;
diff --git a/cmds/statsd/src/condition/CombinationConditionTracker.cpp b/cmds/statsd/src/condition/CombinationConditionTracker.cpp
index 52a1269..69aae3d 100644
--- a/cmds/statsd/src/condition/CombinationConditionTracker.cpp
+++ b/cmds/statsd/src/condition/CombinationConditionTracker.cpp
@@ -86,7 +86,7 @@
             ALOGW("Child initialization failed %lld ", (long long)child);
             return false;
         } else {
-            ALOGW("Child initialization success %lld ", (long long)child);
+            VLOG("Child initialization success %lld ", (long long)child);
         }
 
         if (allConditionTrackers[childIndex]->isSliced()) {
diff --git a/core/java/android/app/timedetector/ManualTimeSuggestion.java b/core/java/android/app/timedetector/ManualTimeSuggestion.java
index e7d619a..471606da 100644
--- a/core/java/android/app/timedetector/ManualTimeSuggestion.java
+++ b/core/java/android/app/timedetector/ManualTimeSuggestion.java
@@ -85,7 +85,8 @@
 
     @NonNull
     public List<String> getDebugInfo() {
-        return Collections.unmodifiableList(mDebugInfo);
+        return mDebugInfo == null
+                ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
     }
 
     /**
diff --git a/core/java/android/app/timedetector/PhoneTimeSuggestion.java b/core/java/android/app/timedetector/PhoneTimeSuggestion.java
index 233dbbc..dd02af7 100644
--- a/core/java/android/app/timedetector/PhoneTimeSuggestion.java
+++ b/core/java/android/app/timedetector/PhoneTimeSuggestion.java
@@ -23,7 +23,6 @@
 import android.util.TimestampedValue;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -52,20 +51,25 @@
             };
 
     private final int mPhoneId;
-    @Nullable private TimestampedValue<Long> mUtcTime;
+    @Nullable private final TimestampedValue<Long> mUtcTime;
     @Nullable private ArrayList<String> mDebugInfo;
 
-    public PhoneTimeSuggestion(int phoneId) {
-        mPhoneId = phoneId;
+    private PhoneTimeSuggestion(Builder builder) {
+        mPhoneId = builder.mPhoneId;
+        mUtcTime = builder.mUtcTime;
+        mDebugInfo = builder.mDebugInfo != null ? new ArrayList<>(builder.mDebugInfo) : null;
     }
 
     private static PhoneTimeSuggestion createFromParcel(Parcel in) {
         int phoneId = in.readInt();
-        PhoneTimeSuggestion suggestion = new PhoneTimeSuggestion(phoneId);
-        suggestion.setUtcTime(in.readParcelable(null /* classLoader */));
+        PhoneTimeSuggestion suggestion = new PhoneTimeSuggestion.Builder(phoneId)
+                .setUtcTime(in.readParcelable(null /* classLoader */))
+                .build();
         @SuppressWarnings("unchecked")
         ArrayList<String> debugInfo = (ArrayList<String>) in.readArrayList(null /* classLoader */);
-        suggestion.mDebugInfo = debugInfo;
+        if (debugInfo != null) {
+            suggestion.addDebugInfo(debugInfo);
+        }
         return suggestion;
     }
 
@@ -85,10 +89,6 @@
         return mPhoneId;
     }
 
-    public void setUtcTime(@Nullable TimestampedValue<Long> utcTime) {
-        mUtcTime = utcTime;
-    }
-
     @Nullable
     public TimestampedValue<Long> getUtcTime() {
         return mUtcTime;
@@ -96,7 +96,8 @@
 
     @NonNull
     public List<String> getDebugInfo() {
-        return Collections.unmodifiableList(mDebugInfo);
+        return mDebugInfo == null
+                ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
     }
 
     /**
@@ -104,11 +105,23 @@
      * information is present in {@link #toString()} but is not considered for
      * {@link #equals(Object)} and {@link #hashCode()}.
      */
-    public void addDebugInfo(String... debugInfos) {
+    public void addDebugInfo(String debugInfo) {
         if (mDebugInfo == null) {
             mDebugInfo = new ArrayList<>();
         }
-        mDebugInfo.addAll(Arrays.asList(debugInfos));
+        mDebugInfo.add(debugInfo);
+    }
+
+    /**
+     * Associates information with the instance that can be useful for debugging / logging. The
+     * information is present in {@link #toString()} but is not considered for
+     * {@link #equals(Object)} and {@link #hashCode()}.
+     */
+    public void addDebugInfo(@NonNull List<String> debugInfo) {
+        if (mDebugInfo == null) {
+            mDebugInfo = new ArrayList<>(debugInfo.size());
+        }
+        mDebugInfo.addAll(debugInfo);
     }
 
     @Override
@@ -137,4 +150,39 @@
                 + ", mDebugInfo=" + mDebugInfo
                 + '}';
     }
+
+    /**
+     * Builds {@link PhoneTimeSuggestion} instances.
+     *
+     * @hide
+     */
+    public static class Builder {
+        private final int mPhoneId;
+        private TimestampedValue<Long> mUtcTime;
+        private List<String> mDebugInfo;
+
+        public Builder(int phoneId) {
+            mPhoneId = phoneId;
+        }
+
+        /** Returns the builder for call chaining. */
+        public Builder setUtcTime(TimestampedValue<Long> utcTime) {
+            mUtcTime = utcTime;
+            return this;
+        }
+
+        /** Returns the builder for call chaining. */
+        public Builder addDebugInfo(@NonNull String debugInfo) {
+            if (mDebugInfo == null) {
+                mDebugInfo = new ArrayList<>();
+            }
+            mDebugInfo.add(debugInfo);
+            return this;
+        }
+
+        /** Returns the {@link PhoneTimeSuggestion}. */
+        public PhoneTimeSuggestion build() {
+            return new PhoneTimeSuggestion(this);
+        }
+    }
 }
diff --git a/core/java/android/content/res/loader/ResourceLoaderManager.java b/core/java/android/content/res/loader/ResourceLoaderManager.java
index ddbfa81..592ec09 100644
--- a/core/java/android/content/res/loader/ResourceLoaderManager.java
+++ b/core/java/android/content/res/loader/ResourceLoaderManager.java
@@ -151,6 +151,7 @@
     public void onImplUpdate(ResourcesImpl resourcesImpl) {
         synchronized (mLock) {
             this.mResourcesImpl = resourcesImpl;
+            this.mResourcesImpl.getAssets().setResourceLoaderManager(this);
             updateLoaders();
         }
     }
diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java
index 1c6a484..bf4884a 100644
--- a/core/java/android/net/TrafficStats.java
+++ b/core/java/android/net/TrafficStats.java
@@ -16,6 +16,7 @@
 
 package android.net;
 
+import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
@@ -595,8 +596,15 @@
         return total;
     }
 
-    /** {@hide} */
-    public static long getTxPackets(String iface) {
+    /**
+     * Return the number of packets transmitted on the specified interface since
+     * device boot. Statistics are measured at the network layer, so both TCP and
+     * UDP usage are included.
+     *
+     * @param iface The name of the interface.
+     * @return The number of transmitted packets.
+     */
+    public static long getTxPackets(@NonNull String iface) {
         try {
             return getStatsService().getIfaceStats(iface, TYPE_TX_PACKETS);
         } catch (RemoteException e) {
@@ -604,8 +612,15 @@
         }
     }
 
-    /** {@hide} */
-    public static long getRxPackets(String iface) {
+    /**
+     * Return the number of packets received on the specified interface since
+     * device boot. Statistics are measured at the network layer, so both TCP
+     * and UDP usage are included.
+     *
+     * @param iface The name of the interface.
+     * @return The number of received packets.
+     */
+    public static long getRxPackets(@NonNull String iface) {
         try {
             return getStatsService().getIfaceStats(iface, TYPE_RX_PACKETS);
         } catch (RemoteException e) {
diff --git a/core/java/android/os/IStatsManager.aidl b/core/java/android/os/IStatsManager.aidl
index 29871b6..5ebb9f2 100644
--- a/core/java/android/os/IStatsManager.aidl
+++ b/core/java/android/os/IStatsManager.aidl
@@ -202,6 +202,13 @@
                            in int[] additiveFields, IPullAtomCallback pullerCallback);
 
    /**
+    * Registers a puller callback function that, when invoked, pulls the data
+    * for the specified atom tag.
+    */
+    oneway void registerNativePullAtomCallback(int atomTag, long coolDownNs, long timeoutNs,
+                           in int[] additiveFields, IPullAtomCallback pullerCallback);
+
+   /**
     * Unregisters a puller callback function for the given vendor atom.
     *
     * Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS
diff --git a/core/java/android/service/autofill/augmented/FillWindow.java b/core/java/android/service/autofill/augmented/FillWindow.java
index 6a29d48..5d00370 100644
--- a/core/java/android/service/autofill/augmented/FillWindow.java
+++ b/core/java/android/service/autofill/augmented/FillWindow.java
@@ -242,6 +242,7 @@
         synchronized (mLock) {
             if (mDestroyed) return;
             if (mUpdateCalled) {
+                mFillView.setOnClickListener(null);
                 hide();
                 mProxy.report(AutofillProxy.REPORT_EVENT_UI_DESTROYED);
             }
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 54446e1..9c04b39 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -47,6 +47,7 @@
 import android.service.autofill.AutofillService;
 import android.service.autofill.FillEventHistory;
 import android.service.autofill.UserData;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.DebugUtils;
@@ -61,6 +62,7 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeProvider;
 import android.view.accessibility.AccessibilityWindowInfo;
+import android.widget.EditText;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.MetricsLogger;
@@ -1073,6 +1075,8 @@
                 } else if (sVerbose) {
                     Log.v(TAG, "Ignoring visibility change on " + id + ": no tracked views");
                 }
+            } else if (!virtual && isVisible) {
+                startAutofillIfNeededLocked(view);
             }
         }
     }
@@ -1238,9 +1242,11 @@
                 return;
             }
             if (!mEnabled || !isActiveLocked()) {
-                if (sVerbose) {
-                    Log.v(TAG, "notifyValueChanged(" + view.getAutofillId()
-                            + "): ignoring on state " + getStateAsStringLocked());
+                if (!startAutofillIfNeededLocked(view)) {
+                    if (sVerbose) {
+                        Log.v(TAG, "notifyValueChanged(" + view.getAutofillId()
+                                + "): ignoring on state " + getStateAsStringLocked());
+                    }
                 }
                 return;
             }
@@ -1879,6 +1885,37 @@
         }
     }
 
+    @GuardedBy("mLock")
+    private boolean startAutofillIfNeededLocked(View view) {
+        if (mState == STATE_UNKNOWN
+                && mSessionId == NO_SESSION
+                && view instanceof EditText
+                && !TextUtils.isEmpty(((EditText) view).getText())
+                && !view.isFocused()
+                && view.isImportantForAutofill()
+                && view.isLaidOut()
+                && view.isVisibleToUser()) {
+
+            ensureServiceClientAddedIfNeededLocked();
+
+            if (sVerbose) {
+                Log.v(TAG, "startAutofillIfNeededLocked(): enabled=" + mEnabled);
+            }
+            if (mEnabled && !isClientDisablingEnterExitEvent()) {
+                final AutofillId id = view.getAutofillId();
+                final AutofillValue value = view.getAutofillValue();
+                // Starts new session.
+                startSessionLocked(id, /* bounds= */ null, /* value= */ null, /* flags= */ 0);
+                // Updates value.
+                updateSessionLocked(id, /* bounds= */ null, value, ACTION_VALUE_CHANGED,
+                        /* flags= */ 0);
+                addEnteredIdLocked(id);
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Registers a {@link AutofillCallback} to receive autofill events.
      *
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 187ab46..eb0d9bf 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -152,6 +152,9 @@
     // handles.
     private static final boolean FLAG_USE_MAGNIFIER = true;
 
+    private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
+    private static final int RECENT_CUT_COPY_DURATION_MS = 15 * 1000; // 15 seconds in millis
+
     static final int BLINK = 500;
     private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
     private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
@@ -326,8 +329,6 @@
     // Global listener that detects changes in the global position of the TextView
     private PositionListener mPositionListener;
 
-    private float mLastDownPositionX, mLastDownPositionY;
-    private float mLastUpPositionX, mLastUpPositionY;
     private float mContextMenuAnchorX, mContextMenuAnchorY;
     Callback mCustomSelectionActionModeCallback;
     Callback mCustomInsertionActionModeCallback;
@@ -336,18 +337,11 @@
     @UnsupportedAppUsage
     boolean mCreatedWithASelection;
 
-    // Indicates the current tap state (first tap, double tap, or triple click).
-    private int mTapState = TAP_STATE_INITIAL;
-    private long mLastTouchUpTime = 0;
-    private static final int TAP_STATE_INITIAL = 0;
-    private static final int TAP_STATE_FIRST_TAP = 1;
-    private static final int TAP_STATE_DOUBLE_TAP = 2;
-    // Only for mouse input.
-    private static final int TAP_STATE_TRIPLE_CLICK = 3;
-
     // The button state as of the last time #onTouchEvent is called.
     private int mLastButtonState;
 
+    private final EditorTouchState mTouchState = new EditorTouchState();
+
     private Runnable mInsertionActionModeRunnable;
 
     // The span controller helps monitoring the changes to which the Editor needs to react:
@@ -1193,10 +1187,10 @@
             logCursor("performLongClick", "handled=%s", handled);
         }
         // Long press in empty space moves cursor and starts the insertion action mode.
-        if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY)
+        if (!handled && !isPositionOnText(mTouchState.getLastDownX(), mTouchState.getLastDownY())
                 && mInsertionControllerEnabled) {
-            final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
-                    mLastDownPositionY);
+            final int offset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(),
+                    mTouchState.getLastDownY());
             Selection.setSelection((Spannable) mTextView.getText(), offset);
             getInsertionController().show();
             mIsInsertionActionModeStartPending = true;
@@ -1240,11 +1234,11 @@
     }
 
     float getLastUpPositionX() {
-        return mLastUpPositionX;
+        return mTouchState.getLastUpX();
     }
 
     float getLastUpPositionY() {
-        return mLastUpPositionY;
+        return mTouchState.getLastUpY();
     }
 
     private long getLastTouchOffsets() {
@@ -1279,6 +1273,9 @@
                 // Has to be done before onTakeFocus, which can be overloaded.
                 final int lastTapPosition = getLastTapPosition();
                 if (lastTapPosition >= 0) {
+                    if (TextView.DEBUG_CURSOR) {
+                        logCursor("onFocusChanged", "setting cursor position: %d", lastTapPosition);
+                    }
                     Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
                 }
 
@@ -1443,39 +1440,6 @@
         }
     }
 
-    private void updateTapState(MotionEvent event) {
-        final int action = event.getActionMasked();
-        if (action == MotionEvent.ACTION_DOWN) {
-            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
-            // Detect double tap and triple click.
-            if (((mTapState == TAP_STATE_FIRST_TAP)
-                    || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
-                            && (SystemClock.uptimeMillis() - mLastTouchUpTime)
-                                    <= ViewConfiguration.getDoubleTapTimeout()) {
-                if (mTapState == TAP_STATE_FIRST_TAP) {
-                    mTapState = TAP_STATE_DOUBLE_TAP;
-                } else {
-                    mTapState = TAP_STATE_TRIPLE_CLICK;
-                }
-                if (TextView.DEBUG_CURSOR) {
-                    logCursor("updateTapState", "ACTION_DOWN: %s tap detected",
-                            (mTapState == TAP_STATE_DOUBLE_TAP ? "double" : "triple"));
-                }
-            } else {
-                mTapState = TAP_STATE_FIRST_TAP;
-                if (TextView.DEBUG_CURSOR) {
-                    logCursor("updateTapState", "ACTION_DOWN: first tap detected");
-                }
-            }
-        }
-        if (action == MotionEvent.ACTION_UP) {
-            mLastTouchUpTime = SystemClock.uptimeMillis();
-            if (TextView.DEBUG_CURSOR) {
-                logCursor("updateTapState", "ACTION_UP");
-            }
-        }
-    }
-
     private boolean shouldFilterOutTouchEvent(MotionEvent event) {
         if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
             return false;
@@ -1503,7 +1467,8 @@
             }
             return;
         }
-        updateTapState(event);
+        ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
+        mTouchState.update(event, viewConfiguration);
         updateFloatingToolbarVisibility(event);
 
         if (hasSelectionController()) {
@@ -1515,15 +1480,7 @@
             mShowSuggestionRunnable = null;
         }
 
-        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
-            mLastUpPositionX = event.getX();
-            mLastUpPositionY = event.getY();
-        }
-
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
-            mLastDownPositionX = event.getX();
-            mLastDownPositionY = event.getY();
-
             // Reset this state; it will be re-set if super.onTouchEvent
             // causes focus to move to the view.
             mTouchFocusSelected = false;
@@ -5067,7 +5024,10 @@
         public boolean onTouchEvent(MotionEvent ev) {
             if (TextView.DEBUG_CURSOR) {
                 logCursor(this.getClass().getSimpleName() + ": HandleView: onTouchEvent",
-                        MotionEvent.actionToString(ev.getActionMasked()));
+                        "%d: %s (%f,%f)",
+                        ev.getSequenceNumber(),
+                        MotionEvent.actionToString(ev.getActionMasked()),
+                        ev.getX(), ev.getY());
             }
 
             updateFloatingToolbarVisibility(ev);
@@ -5145,56 +5105,14 @@
     }
 
     private class InsertionHandleView extends HandleView {
-        private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
-        private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
-
         // Used to detect taps on the insertion handle, which will affect the insertion action mode
-        private float mDownPositionX, mDownPositionY;
+        private float mLastDownRawX, mLastDownRawY;
         private Runnable mHider;
 
         public InsertionHandleView(Drawable drawable) {
             super(drawable, drawable, com.android.internal.R.id.insertion_handle);
         }
 
-        @Override
-        public void show() {
-            super.show();
-
-            final long durationSinceCutOrCopy =
-                    SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
-
-            // Cancel the single tap delayed runnable.
-            if (mInsertionActionModeRunnable != null
-                    && ((mTapState == TAP_STATE_DOUBLE_TAP)
-                            || (mTapState == TAP_STATE_TRIPLE_CLICK)
-                            || isCursorInsideEasyCorrectionSpan())) {
-                mTextView.removeCallbacks(mInsertionActionModeRunnable);
-            }
-
-            // Prepare and schedule the single tap runnable to run exactly after the double tap
-            // timeout has passed.
-            if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
-                    && !isCursorInsideEasyCorrectionSpan()
-                    && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
-                if (mTextActionMode == null) {
-                    if (mInsertionActionModeRunnable == null) {
-                        mInsertionActionModeRunnable = new Runnable() {
-                            @Override
-                            public void run() {
-                                startInsertionActionMode();
-                            }
-                        };
-                    }
-                    mTextView.postDelayed(
-                            mInsertionActionModeRunnable,
-                            ViewConfiguration.getDoubleTapTimeout() + 1);
-                }
-
-            }
-
-            hideAfterDelay();
-        }
-
         private void hideAfterDelay() {
             if (mHider == null) {
                 mHider = new Runnable() {
@@ -5250,8 +5168,8 @@
 
             switch (ev.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN:
-                    mDownPositionX = ev.getRawX();
-                    mDownPositionY = ev.getRawY();
+                    mLastDownRawX = ev.getRawX();
+                    mLastDownRawY = ev.getRawY();
                     updateMagnifier(ev);
                     break;
 
@@ -5261,8 +5179,8 @@
 
                 case MotionEvent.ACTION_UP:
                     if (!offsetHasBeenChanged()) {
-                        final float deltaX = mDownPositionX - ev.getRawX();
-                        final float deltaY = mDownPositionY - ev.getRawY();
+                        final float deltaX = mLastDownRawX - ev.getRawX();
+                        final float deltaY = mLastDownRawY - ev.getRawY();
                         final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
 
                         final ViewConfiguration viewConfiguration = ViewConfiguration.get(
@@ -5804,6 +5722,37 @@
         public void show() {
             getHandle().show();
 
+            final long durationSinceCutOrCopy =
+                    SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
+
+            // Cancel the single tap delayed runnable.
+            if (mInsertionActionModeRunnable != null
+                    && (mTouchState.isMultiTap() || isCursorInsideEasyCorrectionSpan())) {
+                mTextView.removeCallbacks(mInsertionActionModeRunnable);
+            }
+
+            // Prepare and schedule the single tap runnable to run exactly after the double tap
+            // timeout has passed.
+            if (!mTouchState.isMultiTap()
+                    && !isCursorInsideEasyCorrectionSpan()
+                    && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION_MS)) {
+                if (mTextActionMode == null) {
+                    if (mInsertionActionModeRunnable == null) {
+                        mInsertionActionModeRunnable = new Runnable() {
+                            @Override
+                            public void run() {
+                                startInsertionActionMode();
+                            }
+                        };
+                    }
+                    mTextView.postDelayed(
+                            mInsertionActionModeRunnable,
+                            ViewConfiguration.getDoubleTapTimeout() + 1);
+                }
+            }
+
+            getHandle().hideAfterDelay();
+
             if (mSelectionModifierCursorController != null) {
                 mSelectionModifierCursorController.hide();
             }
@@ -5870,7 +5819,6 @@
         // The offsets of that last touch down event. Remembered to start selection there.
         private int mMinTouchOffset, mMaxTouchOffset;
 
-        private float mDownPositionX, mDownPositionY;
         private boolean mGestureStayedInTapRegion;
 
         // Where the user first starts the drag motion.
@@ -5940,13 +5888,18 @@
         }
 
         public void enterDrag(int dragAcceleratorMode) {
+            if (TextView.DEBUG_CURSOR) {
+                logCursor("SelectionModifierCursorController: enterDrag",
+                        "starting selection drag: mode=%s", dragAcceleratorMode);
+            }
+
             // Just need to init the handles / hide insertion cursor.
             show();
             mDragAcceleratorMode = dragAcceleratorMode;
             // Start location of selection.
-            mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
-                    mLastDownPositionY);
-            mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
+            mStartOffset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(),
+                    mTouchState.getLastDownY());
+            mLineSelectionIsOn = mTextView.getLineAtCoordinate(mTouchState.getLastDownY());
             // Don't show the handles until user has lifted finger.
             hide();
 
@@ -5974,36 +5927,20 @@
                                 eventX, eventY);
 
                         // Double tap detection
-                        if (mGestureStayedInTapRegion) {
-                            if (mTapState == TAP_STATE_DOUBLE_TAP
-                                    || mTapState == TAP_STATE_TRIPLE_CLICK) {
-                                final float deltaX = eventX - mDownPositionX;
-                                final float deltaY = eventY - mDownPositionY;
-                                final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
-
-                                ViewConfiguration viewConfiguration = ViewConfiguration.get(
-                                        mTextView.getContext());
-                                int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
-                                boolean stayedInArea =
-                                        distanceSquared < doubleTapSlop * doubleTapSlop;
-
-                                if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
-                                    if (TextView.DEBUG_CURSOR) {
-                                        logCursor("SelectionModifierCursorController: onTouchEvent",
-                                                "ACTION_DOWN: select and start drag");
-                                    }
-                                    if (mTapState == TAP_STATE_DOUBLE_TAP) {
-                                        selectCurrentWordAndStartDrag();
-                                    } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
-                                        selectCurrentParagraphAndStartDrag();
-                                    }
-                                    mDiscardNextActionUp = true;
-                                }
+                        if (mGestureStayedInTapRegion
+                                && mTouchState.isMultiTapInSameArea()
+                                && (isMouse || isPositionOnText(eventX, eventY))) {
+                            if (TextView.DEBUG_CURSOR) {
+                                logCursor("SelectionModifierCursorController: onTouchEvent",
+                                        "ACTION_DOWN: select and start drag");
                             }
+                            if (mTouchState.isDoubleTap()) {
+                                selectCurrentWordAndStartDrag();
+                            } else if (mTouchState.isTripleClick()) {
+                                selectCurrentParagraphAndStartDrag();
+                            }
+                            mDiscardNextActionUp = true;
                         }
-
-                        mDownPositionX = eventX;
-                        mDownPositionY = eventY;
                         mGestureStayedInTapRegion = true;
                         mHaventMovedEnoughToStartDrag = true;
                     }
@@ -6025,8 +5962,8 @@
                     final int touchSlop = viewConfig.getScaledTouchSlop();
 
                     if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
-                        final float deltaX = eventX - mDownPositionX;
-                        final float deltaY = eventY - mDownPositionY;
+                        final float deltaX = eventX - mTouchState.getLastDownX();
+                        final float deltaY = eventY - mTouchState.getLastDownY();
                         final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
 
                         if (mGestureStayedInTapRegion) {
@@ -7164,7 +7101,7 @@
         }
     }
 
-    private static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) {
+    static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) {
         if (msgFormat == null) {
             Log.d(TAG, location);
         } else {
diff --git a/core/java/android/widget/EditorTouchState.java b/core/java/android/widget/EditorTouchState.java
new file mode 100644
index 0000000..f880939
--- /dev/null
+++ b/core/java/android/widget/EditorTouchState.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import static android.widget.Editor.logCursor;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.annotation.IntDef;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper class used by {@link Editor} to track state for touch events.
+ *
+ * @hide
+ */
+@VisibleForTesting(visibility = PACKAGE)
+public class EditorTouchState {
+    private float mLastDownX, mLastDownY;
+    private float mLastUpX, mLastUpY;
+    private long mLastUpMillis;
+
+    @IntDef({MultiTapStatus.NONE, MultiTapStatus.FIRST_TAP, MultiTapStatus.DOUBLE_TAP,
+            MultiTapStatus.TRIPLE_CLICK})
+    @Retention(RetentionPolicy.SOURCE)
+    @VisibleForTesting
+    public @interface MultiTapStatus {
+        int NONE = 0;
+        int FIRST_TAP = 1;
+        int DOUBLE_TAP = 2;
+        int TRIPLE_CLICK = 3; // Only for mouse input.
+    }
+    @MultiTapStatus
+    private int mMultiTapStatus = MultiTapStatus.NONE;
+    private boolean mMultiTapInSameArea;
+
+    public float getLastDownX() {
+        return mLastDownX;
+    }
+
+    public float getLastDownY() {
+        return mLastDownY;
+    }
+
+    public float getLastUpX() {
+        return mLastUpX;
+    }
+
+    public float getLastUpY() {
+        return mLastUpY;
+    }
+
+    public boolean isDoubleTap() {
+        return mMultiTapStatus == MultiTapStatus.DOUBLE_TAP;
+    }
+
+    public boolean isTripleClick() {
+        return mMultiTapStatus == MultiTapStatus.TRIPLE_CLICK;
+    }
+
+    public boolean isMultiTap() {
+        return mMultiTapStatus == MultiTapStatus.DOUBLE_TAP
+                || mMultiTapStatus == MultiTapStatus.TRIPLE_CLICK;
+    }
+
+    public boolean isMultiTapInSameArea() {
+        return isMultiTap() && mMultiTapInSameArea;
+    }
+
+    /**
+     * Updates the state based on the new event.
+     */
+    public void update(MotionEvent event, ViewConfiguration viewConfiguration) {
+        final int action = event.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN) {
+            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
+            final long millisSinceLastUp = event.getEventTime() - mLastUpMillis;
+            // Detect double tap and triple click.
+            if (millisSinceLastUp <= ViewConfiguration.getDoubleTapTimeout()
+                    && (mMultiTapStatus == MultiTapStatus.FIRST_TAP
+                    || (mMultiTapStatus == MultiTapStatus.DOUBLE_TAP && isMouse))) {
+                if (mMultiTapStatus == MultiTapStatus.FIRST_TAP) {
+                    mMultiTapStatus = MultiTapStatus.DOUBLE_TAP;
+                } else {
+                    mMultiTapStatus = MultiTapStatus.TRIPLE_CLICK;
+                }
+                final float deltaX = event.getX() - mLastDownX;
+                final float deltaY = event.getY() - mLastDownY;
+                final int distanceSquared = (int) ((deltaX * deltaX) + (deltaY * deltaY));
+                int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
+                mMultiTapInSameArea = distanceSquared < doubleTapSlop * doubleTapSlop;
+                if (TextView.DEBUG_CURSOR) {
+                    String status = isDoubleTap() ? "double" : "triple";
+                    String inSameArea = mMultiTapInSameArea ? "in same area" : "not in same area";
+                    logCursor("EditorTouchState", "ACTION_DOWN: %s tap detected, %s",
+                            status, inSameArea);
+                }
+            } else {
+                mMultiTapStatus = MultiTapStatus.FIRST_TAP;
+                mMultiTapInSameArea = false;
+                if (TextView.DEBUG_CURSOR) {
+                    logCursor("EditorTouchState", "ACTION_DOWN: first tap detected");
+                }
+            }
+            mLastDownX = event.getX();
+            mLastDownY = event.getY();
+        } else if (action == MotionEvent.ACTION_UP) {
+            if (TextView.DEBUG_CURSOR) {
+                logCursor("EditorTouchState", "ACTION_UP");
+            }
+            mLastUpX = event.getX();
+            mLastUpY = event.getY();
+            mLastUpMillis = event.getEventTime();
+        }
+    }
+}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 90e8ef2..ee169f2 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -10860,7 +10860,10 @@
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         if (DEBUG_CURSOR) {
-            logCursor("onTouchEvent", MotionEvent.actionToString(event.getActionMasked()));
+            logCursor("onTouchEvent", "%d: %s (%f,%f)",
+                    event.getSequenceNumber(),
+                    MotionEvent.actionToString(event.getActionMasked()),
+                    event.getX(), event.getY());
         }
 
         final int action = event.getActionMasked();
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt
index e01e254..0c3d34e 100644
--- a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt
@@ -31,7 +31,6 @@
 import androidx.test.runner.lifecycle.Stage
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
-import org.junit.Assume
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -43,7 +42,6 @@
 import java.util.concurrent.FutureTask
 import java.util.concurrent.TimeUnit
 
-// @Ignore("UiAutomation is crashing with not connected, not sure why")
 @RunWith(Parameterized::class)
 class ResourceLoaderChangesTest : ResourceLoaderTestBase() {
 
@@ -75,7 +73,7 @@
     @Before
     @After
     fun disableOverlay() {
-//        enableOverlay(OVERLAY_PACKAGE, false)
+        enableOverlay(OVERLAY_PACKAGE, false)
     }
 
     @Test
@@ -156,25 +154,32 @@
 
     // All these tests assert for the exact same loaders/values, so extract that logic out
     private fun verifySameBeforeAndAfter(block: () -> Resources) {
-        // TODO(chiuwinson): atest doesn't work with @Ignore, UiAutomation not connected error
-        Assume.assumeFalse(true)
+        fun Resources.resource() = this.getString(android.R.string.cancel)
+        fun Resources.asset() = this.assets.open("Asset.txt").reader().readText()
 
-        val originalValue = resources.getString(android.R.string.cancel)
+        val originalResource = resources.resource()
+        val originalAsset = resources.asset()
 
-        val loader = "stringOne".openLoader()
-        addLoader(loader)
+        val loaderResource = "stringOne".openLoader()
+        val loaderAsset = "assetOne".openLoader(dataType = DataType.ASSET)
+        addLoader(loaderResource)
+        addLoader(loaderAsset)
 
         val oldLoaders = resources.loaders
-        val oldValue = resources.getString(android.R.string.cancel)
+        val oldResource = resources.resource()
+        val oldAsset = resources.asset()
 
-        assertThat(oldValue).isNotEqualTo(originalValue)
+        assertThat(oldResource).isNotEqualTo(originalResource)
+        assertThat(oldAsset).isNotEqualTo(originalAsset)
 
         val newResources = block()
 
         val newLoaders = newResources.loaders
-        val newValue = newResources.getString(android.R.string.cancel)
+        val newResource = newResources.resource()
+        val newAsset = newResources.asset()
 
-        assertThat(newValue).isEqualTo(oldValue)
+        assertThat(newResource).isEqualTo(oldResource)
+        assertThat(newAsset).isEqualTo(oldAsset)
         assertThat(newLoaders).isEqualTo(oldLoaders)
     }
 
diff --git a/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java
new file mode 100644
index 0000000..de6f8f7
--- /dev/null
+++ b/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.timedetector;
+
+import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.util.TimestampedValue;
+
+import org.junit.Test;
+
+public class ManualTimeSuggestionTest {
+
+    private static final TimestampedValue<Long> ARBITRARY_TIME =
+            new TimestampedValue<>(1111L, 2222L);
+
+    @Test
+    public void testEquals() {
+        ManualTimeSuggestion one = new ManualTimeSuggestion(ARBITRARY_TIME);
+        assertEquals(one, one);
+
+        ManualTimeSuggestion two = new ManualTimeSuggestion(ARBITRARY_TIME);
+        assertEquals(one, two);
+        assertEquals(two, one);
+
+        TimestampedValue<Long> differentTime = new TimestampedValue<>(
+                ARBITRARY_TIME.getReferenceTimeMillis() + 1,
+                ARBITRARY_TIME.getValue());
+        ManualTimeSuggestion three = new ManualTimeSuggestion(differentTime);
+        assertNotEquals(one, three);
+        assertNotEquals(three, one);
+
+        // DebugInfo must not be considered in equals().
+        one.addDebugInfo("Debug info 1");
+        two.addDebugInfo("Debug info 2");
+        assertEquals(one, two);
+    }
+
+    @Test
+    public void testParcelable() {
+        ManualTimeSuggestion suggestion = new ManualTimeSuggestion(ARBITRARY_TIME);
+        assertRoundTripParcelable(suggestion);
+
+        // DebugInfo should also be stored (but is not checked by equals()
+        suggestion.addDebugInfo("This is debug info");
+        ManualTimeSuggestion rtSuggestion = roundTripParcelable(suggestion);
+        assertEquals(suggestion.getDebugInfo(), rtSuggestion.getDebugInfo());
+    }
+}
diff --git a/core/tests/coretests/src/android/app/timedetector/PhoneTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/PhoneTimeSuggestionTest.java
index c9a86dc..bee270e 100644
--- a/core/tests/coretests/src/android/app/timedetector/PhoneTimeSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timedetector/PhoneTimeSuggestionTest.java
@@ -16,11 +16,12 @@
 
 package android.app.timedetector;
 
+import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
-import android.os.Parcel;
-import android.os.Parcelable;
 import android.util.TimestampedValue;
 
 import org.junit.Test;
@@ -30,53 +31,67 @@
 
     @Test
     public void testEquals() {
-        PhoneTimeSuggestion one = new PhoneTimeSuggestion(PHONE_ID);
-        assertEquals(one, one);
+        PhoneTimeSuggestion.Builder builder1 = new PhoneTimeSuggestion.Builder(PHONE_ID);
+        {
+            PhoneTimeSuggestion one = builder1.build();
+            assertEquals(one, one);
+        }
 
-        PhoneTimeSuggestion two = new PhoneTimeSuggestion(PHONE_ID);
-        assertEquals(one, two);
-        assertEquals(two, one);
+        PhoneTimeSuggestion.Builder builder2 = new PhoneTimeSuggestion.Builder(PHONE_ID);
+        {
+            PhoneTimeSuggestion one = builder1.build();
+            PhoneTimeSuggestion two = builder2.build();
+            assertEquals(one, two);
+            assertEquals(two, one);
+        }
 
-        one.setUtcTime(new TimestampedValue<>(1111L, 2222L));
-        assertEquals(one, one);
+        builder1.setUtcTime(new TimestampedValue<>(1111L, 2222L));
+        {
+            PhoneTimeSuggestion one = builder1.build();
+            assertEquals(one, one);
+        }
 
-        two.setUtcTime(new TimestampedValue<>(1111L, 2222L));
-        assertEquals(one, two);
-        assertEquals(two, one);
+        builder2.setUtcTime(new TimestampedValue<>(1111L, 2222L));
+        {
+            PhoneTimeSuggestion one = builder1.build();
+            PhoneTimeSuggestion two = builder2.build();
+            assertEquals(one, two);
+            assertEquals(two, one);
+        }
 
-        PhoneTimeSuggestion three = new PhoneTimeSuggestion(PHONE_ID + 1);
-        three.setUtcTime(new TimestampedValue<>(1111L, 2222L));
-        assertNotEquals(one, three);
-        assertNotEquals(three, one);
+        PhoneTimeSuggestion.Builder builder3 = new PhoneTimeSuggestion.Builder(PHONE_ID + 1);
+        builder3.setUtcTime(new TimestampedValue<>(1111L, 2222L));
+        {
+            PhoneTimeSuggestion one = builder1.build();
+            PhoneTimeSuggestion three = builder3.build();
+            assertNotEquals(one, three);
+            assertNotEquals(three, one);
+        }
 
         // DebugInfo must not be considered in equals().
-        one.addDebugInfo("Debug info 1");
-        two.addDebugInfo("Debug info 2");
-        assertEquals(one, two);
+        builder1.addDebugInfo("Debug info 1");
+        builder2.addDebugInfo("Debug info 2");
+        {
+            PhoneTimeSuggestion one = builder1.build();
+            PhoneTimeSuggestion two = builder2.build();
+            assertEquals(one, two);
+        }
     }
 
     @Test
     public void testParcelable() {
-        PhoneTimeSuggestion one = new PhoneTimeSuggestion(PHONE_ID);
-        assertEquals(one, roundTripParcelable(one));
+        PhoneTimeSuggestion.Builder builder = new PhoneTimeSuggestion.Builder(PHONE_ID);
+        assertRoundTripParcelable(builder.build());
 
-        one.setUtcTime(new TimestampedValue<>(1111L, 2222L));
-        assertEquals(one, roundTripParcelable(one));
+        builder.setUtcTime(new TimestampedValue<>(1111L, 2222L));
+        assertRoundTripParcelable(builder.build());
 
         // DebugInfo should also be stored (but is not checked by equals()
-        one.addDebugInfo("This is debug info");
-        PhoneTimeSuggestion two = roundTripParcelable(one);
-        assertEquals(one.getDebugInfo(), two.getDebugInfo());
-    }
-
-    @SuppressWarnings("unchecked")
-    private static <T extends Parcelable> T roundTripParcelable(T one) {
-        Parcel parcel = Parcel.obtain();
-        parcel.writeTypedObject(one, 0);
-        parcel.setDataPosition(0);
-
-        T toReturn = (T) parcel.readTypedObject(PhoneTimeSuggestion.CREATOR);
-        parcel.recycle();
-        return toReturn;
+        {
+            PhoneTimeSuggestion suggestion1 = builder.build();
+            builder.addDebugInfo("This is debug info");
+            PhoneTimeSuggestion rtSuggestion1 = roundTripParcelable(suggestion1);
+            assertEquals(suggestion1.getDebugInfo(), rtSuggestion1.getDebugInfo());
+        }
     }
 }
diff --git a/core/tests/coretests/src/android/widget/EditorTouchStateTest.java b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java
new file mode 100644
index 0000000..6d50e3a
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.widget.EditorTouchState.MultiTapStatus;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+@SmallTest
+public class EditorTouchStateTest {
+
+    private EditorTouchState mTouchState;
+    private ViewConfiguration mConfig;
+
+    @Before
+    public void before() throws Exception {
+        mTouchState = new EditorTouchState();
+        mConfig = new ViewConfiguration();
+    }
+
+    @Test
+    public void testUpdate_singleTap() throws Exception {
+        // Simulate an ACTION_DOWN event.
+        long event1Time = 1000;
+        MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
+        mTouchState.update(event1, mConfig);
+        assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+
+        // Simulate an ACTION_UP event.
+        long event2Time = 1001;
+        MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
+        mTouchState.update(event2, mConfig);
+        assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+
+        // Generate an ACTION_DOWN event whose time is after the double-tap timeout.
+        long event3Time = event2Time + ViewConfiguration.getDoubleTapTimeout() + 1;
+        MotionEvent event3 = downEvent(event3Time, event3Time, 22f, 33f);
+        mTouchState.update(event3, mConfig);
+        assertSingleTap(mTouchState, 22f, 33f, 20f, 30f);
+    }
+
+    @Test
+    public void testUpdate_doubleTap_sameArea() throws Exception {
+        // Simulate an ACTION_DOWN event.
+        long event1Time = 1000;
+        MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
+        mTouchState.update(event1, mConfig);
+        assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+
+        // Simulate an ACTION_UP event.
+        long event2Time = 1001;
+        MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
+        mTouchState.update(event2, mConfig);
+        assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+
+        // Generate an ACTION_DOWN event whose time is within the double-tap timeout.
+        long event3Time = 1002;
+        MotionEvent event3 = downEvent(event3Time, event3Time, 22f, 33f);
+        mTouchState.update(event3, mConfig);
+        assertTap(mTouchState, 22f, 33f, 20f, 30f,
+                MultiTapStatus.DOUBLE_TAP, true);
+    }
+
+    @Test
+    public void testUpdate_doubleTap_notSameArea() throws Exception {
+        // Simulate an ACTION_DOWN event.
+        long event1Time = 1000;
+        MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
+        mTouchState.update(event1, mConfig);
+        assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+
+        // Simulate an ACTION_UP event.
+        long event2Time = 1001;
+        MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
+        mTouchState.update(event2, mConfig);
+        assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+
+        // Generate an ACTION_DOWN event whose time is within the double-tap timeout.
+        long event3Time = 1002;
+        MotionEvent event3 = downEvent(event3Time, event3Time, 200f, 300f);
+        mTouchState.update(event3, mConfig);
+        assertTap(mTouchState, 200f, 300f, 20f, 30f,
+                MultiTapStatus.DOUBLE_TAP, false);
+
+        // Simulate an ACTION_UP event.
+        long event4Time = 1003;
+        MotionEvent event4 = upEvent(event3Time, event4Time, 200f, 300f);
+        mTouchState.update(event4, mConfig);
+        assertTap(mTouchState, 200f, 300f, 200f, 300f,
+                MultiTapStatus.DOUBLE_TAP, false);
+    }
+
+    @Test
+    public void testUpdate_tripleClick_mouse() throws Exception {
+        // Simulate an ACTION_DOWN event.
+        long event1Time = 1000;
+        MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
+        event1.setSource(InputDevice.SOURCE_MOUSE);
+        mTouchState.update(event1, mConfig);
+        assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+
+        // Simulate an ACTION_UP event.
+        long event2Time = 1001;
+        MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
+        event2.setSource(InputDevice.SOURCE_MOUSE);
+        mTouchState.update(event2, mConfig);
+        assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+
+        // Generate a second ACTION_DOWN event whose time is within the double-tap timeout.
+        long event3Time = 1002;
+        MotionEvent event3 = downEvent(event3Time, event3Time, 21f, 31f);
+        event3.setSource(InputDevice.SOURCE_MOUSE);
+        mTouchState.update(event3, mConfig);
+        assertTap(mTouchState, 21f, 31f, 20f, 30f,
+                MultiTapStatus.DOUBLE_TAP, true);
+
+        // Simulate an ACTION_UP event.
+        long event4Time = 1003;
+        MotionEvent event4 = upEvent(event3Time, event4Time, 21f, 31f);
+        event4.setSource(InputDevice.SOURCE_MOUSE);
+        mTouchState.update(event4, mConfig);
+        assertTap(mTouchState, 21f, 31f, 21f, 31f,
+                MultiTapStatus.DOUBLE_TAP, true);
+
+        // Generate a third ACTION_DOWN event whose time is within the double-tap timeout.
+        long event5Time = 1004;
+        MotionEvent event5 = downEvent(event5Time, event5Time, 22f, 32f);
+        event5.setSource(InputDevice.SOURCE_MOUSE);
+        mTouchState.update(event5, mConfig);
+        assertTap(mTouchState, 22f, 32f, 21f, 31f,
+                MultiTapStatus.TRIPLE_CLICK, true);
+    }
+
+    @Test
+    public void testUpdate_tripleClick_touch() throws Exception {
+        // Simulate an ACTION_DOWN event.
+        long event1Time = 1000;
+        MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
+        mTouchState.update(event1, mConfig);
+        assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+
+        // Simulate an ACTION_UP event.
+        long event2Time = 1001;
+        MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
+        mTouchState.update(event2, mConfig);
+        assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+
+        // Generate a second ACTION_DOWN event whose time is within the double-tap timeout.
+        long event3Time = 1002;
+        MotionEvent event3 = downEvent(event3Time, event3Time, 21f, 31f);
+        mTouchState.update(event3, mConfig);
+        assertTap(mTouchState, 21f, 31f, 20f, 30f,
+                MultiTapStatus.DOUBLE_TAP, true);
+
+        // Simulate an ACTION_UP event.
+        long event4Time = 1003;
+        MotionEvent event4 = upEvent(event3Time, event4Time, 21f, 31f);
+        mTouchState.update(event4, mConfig);
+        assertTap(mTouchState, 21f, 31f, 21f, 31f,
+                MultiTapStatus.DOUBLE_TAP, true);
+
+        // Generate a third ACTION_DOWN event whose time is within the double-tap timeout.
+        long event5Time = 1004;
+        MotionEvent event5 = downEvent(event5Time, event5Time, 22f, 32f);
+        mTouchState.update(event5, mConfig);
+        assertTap(mTouchState, 22f, 32f, 21f, 31f,
+                MultiTapStatus.FIRST_TAP, false);
+    }
+
+    private static MotionEvent downEvent(long downTime, long eventTime, float x, float y) {
+        return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0);
+    }
+
+    private static MotionEvent upEvent(long downTime, long eventTime, float x, float y) {
+        return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0);
+    }
+
+    private static void assertSingleTap(EditorTouchState touchState, float lastDownX,
+            float lastDownY, float lastUpX, float lastUpY) {
+        assertThat(touchState.getLastDownX(), is(lastDownX));
+        assertThat(touchState.getLastDownY(), is(lastDownY));
+        assertThat(touchState.getLastUpX(), is(lastUpX));
+        assertThat(touchState.getLastUpY(), is(lastUpY));
+        assertThat(touchState.isDoubleTap(), is(false));
+        assertThat(touchState.isTripleClick(), is(false));
+        assertThat(touchState.isMultiTap(), is(false));
+        assertThat(touchState.isMultiTapInSameArea(), is(false));
+    }
+
+    private static void assertTap(EditorTouchState touchState,
+            float lastDownX, float lastDownY, float lastUpX, float lastUpY,
+            @MultiTapStatus int multiTapStatus, boolean isMultiTapInSameArea) {
+        assertThat(touchState.getLastDownX(), is(lastDownX));
+        assertThat(touchState.getLastDownY(), is(lastDownY));
+        assertThat(touchState.getLastUpX(), is(lastUpX));
+        assertThat(touchState.getLastUpY(), is(lastUpY));
+        assertThat(touchState.isDoubleTap(), is(multiTapStatus == MultiTapStatus.DOUBLE_TAP));
+        assertThat(touchState.isTripleClick(), is(multiTapStatus == MultiTapStatus.TRIPLE_CLICK));
+        assertThat(touchState.isMultiTap(), is(multiTapStatus == MultiTapStatus.DOUBLE_TAP
+                || multiTapStatus == MultiTapStatus.TRIPLE_CLICK));
+        assertThat(touchState.isMultiTapInSameArea(), is(isMultiTapInSameArea));
+    }
+}
diff --git a/packages/SystemUI/res/layout/bubble_view.xml b/packages/SystemUI/res/layout/bubble_view.xml
index e2dea45..78f7cff 100644
--- a/packages/SystemUI/res/layout/bubble_view.xml
+++ b/packages/SystemUI/res/layout/bubble_view.xml
@@ -14,16 +14,8 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-<com.android.systemui.bubbles.BubbleView
+<com.android.systemui.bubbles.BadgedImageView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_height="wrap_content"
-    android:layout_width="wrap_content"
-    android:id="@+id/bubble_view">
-
-    <com.android.systemui.bubbles.BadgedImageView
-        android:id="@+id/bubble_image"
-        android:layout_width="@dimen/individual_bubble_size"
-        android:layout_height="@dimen/individual_bubble_size"
-        android:clipToPadding="false"/>
-
-</com.android.systemui.bubbles.BubbleView>
+    android:id="@+id/bubble_view"
+    android:layout_width="@dimen/individual_bubble_size"
+    android:layout_height="@dimen/individual_bubble_size"/>
diff --git a/packages/SystemUI/res/values/arrays_tv.xml b/packages/SystemUI/res/values/arrays_tv.xml
index 1fe6141..95716c8 100644
--- a/packages/SystemUI/res/values/arrays_tv.xml
+++ b/packages/SystemUI/res/values/arrays_tv.xml
@@ -36,5 +36,6 @@
 
     <string-array name="audio_recording_disclosure_exempt_apps" translatable="false">
       <item>com.google.android.katniss</item>
+      <item>com.google.android.apps.mediashell</item>
     </string-array>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
index c0053d1..a6a3ce0 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
@@ -15,35 +15,61 @@
  */
 package com.android.systemui.bubbles;
 
+import android.annotation.Nullable;
+import android.app.Notification;
 import android.content.Context;
-import android.content.res.TypedArray;
+import android.content.pm.LauncherApps;
 import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
 import android.graphics.Path;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
 import android.util.AttributeSet;
+import android.util.PathParser;
 import android.widget.ImageView;
 
 import com.android.internal.graphics.ColorUtils;
+import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.DotRenderer;
+import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 
 /**
- * View that circle crops its contents and supports displaying a coloured dot on a top corner.
+ * View that displays an adaptive icon with an app-badge and a dot.
+ *
+ * Dot = a small colored circle that indicates whether this bubble has an unread update.
+ * Badge = the icon associated with the app that created this bubble, this will show work profile
+ * badge if appropriate.
  */
 public class BadgedImageView extends ImageView {
 
-    private Rect mTempBounds = new Rect();
+    /** Same value as Launcher3 dot code */
+    private static final float WHITE_SCRIM_ALPHA = 0.54f;
+    /** Same as value in Launcher3 IconShape */
+    private static final int DEFAULT_PATH_SIZE = 100;
 
+    static final int DOT_STATE_DEFAULT = 0;
+    static final int DOT_STATE_SUPPRESSED_FOR_FLYOUT = 1;
+    static final int DOT_STATE_ANIMATING = 2;
+
+    // Flyout gets shown before the dot
+    private int mCurrentDotState = DOT_STATE_SUPPRESSED_FOR_FLYOUT;
+
+    private Bubble mBubble;
+    private BubbleIconFactory mBubbleIconFactory;
+
+    private int mIconBitmapSize;
     private DotRenderer mDotRenderer;
     private DotRenderer.DrawParams mDrawParams;
-    private int mIconBitmapSize;
-    private int mDotColor;
-    private float mDotScale = 0f;
-    private boolean mShowDot;
     private boolean mOnLeft;
 
-    /** Same as value in Launcher3 IconShape */
-    static final int DEFAULT_PATH_SIZE = 100;
+    private int mDotColor;
+    private float mDotScale = 0f;
+    private boolean mDotDrawn;
+
+    private Rect mTempBounds = new Rect();
 
     public BadgedImageView(Context context) {
         this(context, null);
@@ -63,17 +89,19 @@
         mIconBitmapSize = getResources().getDimensionPixelSize(R.dimen.bubble_icon_bitmap_size);
         mDrawParams = new DotRenderer.DrawParams();
 
-        TypedArray ta = context.obtainStyledAttributes(
-                new int[]{android.R.attr.colorBackgroundFloating});
-        ta.recycle();
+        Path iconPath = PathParser.createPathFromPathData(
+                getResources().getString(com.android.internal.R.string.config_icon_mask));
+        mDotRenderer = new DotRenderer(mIconBitmapSize, iconPath, DEFAULT_PATH_SIZE);
     }
 
     @Override
     public void onDraw(Canvas canvas) {
         super.onDraw(canvas);
-        if (!mShowDot) {
+        if (isDotHidden()) {
+            mDotDrawn = false;
             return;
         }
+        mDotDrawn = mDotScale > 0.1f;
         getDrawingRect(mTempBounds);
 
         mDrawParams.color = mDotColor;
@@ -81,16 +109,29 @@
         mDrawParams.leftAlign = mOnLeft;
         mDrawParams.scale = mDotScale;
 
-        if (mDotRenderer == null) {
-            Path circlePath = new Path();
-            float radius = DEFAULT_PATH_SIZE * 0.5f;
-            circlePath.addCircle(radius /* x */, radius /* y */, radius, Path.Direction.CW);
-            mDotRenderer = new DotRenderer(mIconBitmapSize, circlePath, DEFAULT_PATH_SIZE);
-        }
         mDotRenderer.draw(canvas, mDrawParams);
     }
 
     /**
+     * Sets the dot state, does not animate changes.
+     */
+    void setDotState(int state) {
+        mCurrentDotState = state;
+        if (state == DOT_STATE_SUPPRESSED_FOR_FLYOUT || state == DOT_STATE_DEFAULT) {
+            mDotScale = mBubble.showDot() ? 1f : 0f;
+            invalidate();
+        }
+    }
+
+    /**
+     * Whether the dot should be hidden based on current dot state.
+     */
+    private boolean isDotHidden() {
+        return (mCurrentDotState == DOT_STATE_DEFAULT && !mBubble.showDot())
+                || mCurrentDotState == DOT_STATE_SUPPRESSED_FOR_FLYOUT;
+    }
+
+    /**
      * Set whether the dot should appear on left or right side of the view.
      */
     void setDotOnLeft(boolean onLeft) {
@@ -98,29 +139,10 @@
         invalidate();
     }
 
-    boolean getDotOnLeft() {
-        return mOnLeft;
-    }
-
-    /**
-     * Set whether the dot should show or not.
-     */
-    void setShowDot(boolean showDot) {
-        mShowDot = showDot;
-        invalidate();
-    }
-
-    /**
-     * @return whether the dot is being displayed.
-     */
-    boolean isShowingDot() {
-        return mShowDot;
-    }
-
     /**
      * The colour to use for the dot.
      */
-    public void setDotColor(int color) {
+    void setDotColor(int color) {
         mDotColor = ColorUtils.setAlphaComponent(color, 255 /* alpha */);
         invalidate();
     }
@@ -128,7 +150,7 @@
     /**
      * @param iconPath The new icon path to use when calculating dot position.
      */
-    public void drawDot(Path iconPath) {
+    void drawDot(Path iconPath) {
         mDotRenderer = new DotRenderer(mIconBitmapSize, iconPath, DEFAULT_PATH_SIZE);
         invalidate();
     }
@@ -142,6 +164,13 @@
     }
 
     /**
+     * Whether decorations (badges or dots) are on the left.
+     */
+    boolean getDotOnLeft() {
+        return mOnLeft;
+    }
+
+    /**
      * Return dot position relative to bubble view container bounds.
      */
     float[] getDotCenter() {
@@ -149,11 +178,146 @@
         if (mOnLeft) {
             dotPosition = mDotRenderer.getLeftDotPosition();
         } else {
-            dotPosition =  mDotRenderer.getRightDotPosition();
+            dotPosition = mDotRenderer.getRightDotPosition();
         }
         getDrawingRect(mTempBounds);
         float dotCenterX = mTempBounds.width() * dotPosition[0];
         float dotCenterY = mTempBounds.height() * dotPosition[1];
         return new float[]{dotCenterX, dotCenterY};
     }
+
+    /**
+     * Populates this view with a bubble.
+     * <p>
+     * This should only be called when a new bubble is being set on the view, updates to the
+     * current bubble should use {@link #update(Bubble)}.
+     *
+     * @param bubble the bubble to display in this view.
+     */
+    public void setBubble(Bubble bubble) {
+        mBubble = bubble;
+    }
+
+    /**
+     * @param factory Factory for creating normalized bubble icons.
+     */
+    public void setBubbleIconFactory(BubbleIconFactory factory) {
+        mBubbleIconFactory = factory;
+    }
+
+    /**
+     * The key for the {@link Bubble} associated with this view, if one exists.
+     */
+    @Nullable
+    public String getKey() {
+        return (mBubble != null) ? mBubble.getKey() : null;
+    }
+
+    /**
+     * Updates the UI based on the bubble, updates badge and animates messages as needed.
+     */
+    public void update(Bubble bubble) {
+        mBubble = bubble;
+        setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
+        updateViews();
+    }
+
+    int getDotColor() {
+        return mDotColor;
+    }
+
+    /** Sets the position of the 'new' dot, animating it out and back in if requested. */
+    void setDotPosition(boolean onLeft, boolean animate) {
+        if (animate && onLeft != getDotOnLeft() && !isDotHidden()) {
+            animateDot(false /* showDot */, () -> {
+                setDotOnLeft(onLeft);
+                animateDot(true /* showDot */, null);
+            });
+        } else {
+            setDotOnLeft(onLeft);
+        }
+    }
+
+    boolean getDotPositionOnLeft() {
+        return getDotOnLeft();
+    }
+
+    /** Changes the dot's visibility to match the bubble view's state. */
+    void animateDot() {
+        if (mCurrentDotState == DOT_STATE_DEFAULT) {
+            animateDot(mBubble.showDot(), null);
+        }
+    }
+
+    /**
+     * Animates the dot to show or hide.
+     */
+    private void animateDot(boolean showDot, Runnable after) {
+        if (mDotDrawn == showDot) {
+            // State is consistent, do nothing.
+            return;
+        }
+
+        setDotState(DOT_STATE_ANIMATING);
+
+        // Do NOT wait until after animation ends to setShowDot
+        // to avoid overriding more recent showDot states.
+        clearAnimation();
+        animate().setDuration(200)
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                .setUpdateListener((valueAnimator) -> {
+                    float fraction = valueAnimator.getAnimatedFraction();
+                    fraction = showDot ? fraction : 1f - fraction;
+                    setDotScale(fraction);
+                }).withEndAction(() -> {
+                    setDotScale(showDot ? 1f : 0f);
+                    setDotState(DOT_STATE_DEFAULT);
+                    if (after != null) {
+                        after.run();
+                    }
+                }).start();
+    }
+
+    void updateViews() {
+        if (mBubble == null || mBubbleIconFactory == null) {
+            return;
+        }
+
+        Drawable bubbleDrawable = getBubbleDrawable(mContext);
+        BitmapInfo badgeBitmapInfo = mBubbleIconFactory.getBadgedBitmap(mBubble);
+        BitmapInfo bubbleBitmapInfo = mBubbleIconFactory.getBubbleBitmap(bubbleDrawable,
+                badgeBitmapInfo);
+        setImageBitmap(bubbleBitmapInfo.icon);
+
+        // Update badge.
+        mDotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, Color.WHITE, WHITE_SCRIM_ALPHA);
+        setDotColor(mDotColor);
+
+        // Update dot.
+        Path iconPath = PathParser.createPathFromPathData(
+                getResources().getString(com.android.internal.R.string.config_icon_mask));
+        Matrix matrix = new Matrix();
+        float scale = mBubbleIconFactory.getNormalizer().getScale(bubbleDrawable,
+                null /* outBounds */, null /* path */, null /* outMaskShape */);
+        float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f;
+        matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
+                radius /* pivot y */);
+        iconPath.transform(matrix);
+        drawDot(iconPath);
+
+        animateDot();
+    }
+
+    Drawable getBubbleDrawable(Context context) {
+        if (mBubble.getShortcutInfo() != null && mBubble.usingShortcutInfo()) {
+            LauncherApps launcherApps =
+                    (LauncherApps) getContext().getSystemService(Context.LAUNCHER_APPS_SERVICE);
+            int density = getContext().getResources().getConfiguration().densityDpi;
+            return launcherApps.getShortcutIconDrawable(mBubble.getShortcutInfo(), density);
+        } else {
+            Notification.BubbleMetadata metadata = mBubble.getEntry().getBubbleMetadata();
+            Icon ic = metadata.getIcon();
+            return ic.loadDrawable(context);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 46d060e..f3a7ca9 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -64,8 +64,9 @@
     private ShortcutInfo mShortcutInfo;
 
     private boolean mInflated;
-    private BubbleView mIconView;
+    private BadgedImageView mIconView;
     private BubbleExpandedView mExpandedView;
+    private BubbleIconFactory mBubbleIconFactory;
 
     private long mLastUpdated;
     private long mLastAccessed;
@@ -146,7 +147,7 @@
         return mAppName;
     }
 
-    public Drawable getUserBadgedAppIcon() {
+    Drawable getUserBadgedAppIcon() {
         return mUserBadgedAppIcon;
     }
 
@@ -165,17 +166,15 @@
         return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent());
     }
 
+    void setBubbleIconFactory(BubbleIconFactory factory) {
+        mBubbleIconFactory = factory;
+    }
+
     boolean isInflated() {
         return mInflated;
     }
 
-    void updateDotVisibility() {
-        if (mIconView != null) {
-            mIconView.updateDotVisibility(true /* animate */);
-        }
-    }
-
-    BubbleView getIconView() {
+    BadgedImageView getIconView() {
         return mIconView;
     }
 
@@ -193,8 +192,9 @@
         if (mInflated) {
             return;
         }
-        mIconView = (BubbleView) inflater.inflate(
+        mIconView = (BadgedImageView) inflater.inflate(
                 R.layout.bubble_view, stackView, false /* attachToRoot */);
+        mIconView.setBubbleIconFactory(mBubbleIconFactory);
         mIconView.setBubble(this);
 
         mExpandedView = (BubbleExpandedView) inflater.inflate(
@@ -260,15 +260,15 @@
      */
     void markAsAccessedAt(long lastAccessedMillis) {
         mLastAccessed = lastAccessedMillis;
-        setShowInShadeWhenBubble(false);
-        setShowBubbleDot(false);
+        setShowInShade(false);
+        setShowDot(false /* show */, true /* animate */);
     }
 
     /**
      * Whether this notification should be shown in the shade when it is also displayed as a
      * bubble.
      */
-    boolean showInShadeWhenBubble() {
+    boolean showInShade() {
         return !mEntry.isRowDismissed() && !shouldSuppressNotification()
                 && (!mEntry.isClearable() || mShowInShadeWhenBubble);
     }
@@ -277,28 +277,33 @@
      * Sets whether this notification should be shown in the shade when it is also displayed as a
      * bubble.
      */
-    void setShowInShadeWhenBubble(boolean showInShade) {
+    void setShowInShade(boolean showInShade) {
         mShowInShadeWhenBubble = showInShade;
     }
 
     /**
      * Sets whether the bubble for this notification should show a dot indicating updated content.
      */
-    void setShowBubbleDot(boolean showDot) {
+    void setShowDot(boolean showDot, boolean animate) {
         mShowBubbleUpdateDot = showDot;
+        if (animate && mIconView != null) {
+            mIconView.animateDot();
+        } else if (mIconView != null) {
+            mIconView.invalidate();
+        }
     }
 
     /**
      * Whether the bubble for this notification should show a dot indicating updated content.
      */
-    boolean showBubbleDot() {
+    boolean showDot() {
         return mShowBubbleUpdateDot && !mEntry.shouldSuppressNotificationDot();
     }
 
     /**
      * Whether the flyout for the bubble should be shown.
      */
-    boolean showFlyoutForBubble() {
+    boolean showFlyout() {
         return !mSuppressFlyout && !mEntry.shouldSuppressPeek()
                 && !mEntry.shouldSuppressNotificationList();
     }
@@ -470,9 +475,9 @@
     public void dump(
             @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
         pw.print("key: "); pw.println(mKey);
-        pw.print("  showInShade:   "); pw.println(showInShadeWhenBubble());
-        pw.print("  showDot:       "); pw.println(showBubbleDot());
-        pw.print("  showFlyout:    "); pw.println(showFlyoutForBubble());
+        pw.print("  showInShade:   "); pw.println(showInShade());
+        pw.print("  showDot:       "); pw.println(showDot());
+        pw.print("  showFlyout:    "); pw.println(showFlyout());
         pw.print("  desiredHeight: "); pw.println(getDesiredHeightString());
         pw.print("  suppressNotif: "); pw.println(shouldSuppressNotification());
         pw.print("  autoExpand:    "); pw.println(shouldAutoExpand());
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 15bb330..dbb1936 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -251,15 +251,15 @@
         mZenModeController.addCallback(new ZenModeController.Callback() {
             @Override
             public void onZenChanged(int zen) {
-                if (mStackView != null) {
-                    mStackView.updateDots();
+                for (Bubble b : mBubbleData.getBubbles()) {
+                    b.setShowDot(b.showInShade(), true /* animate */);
                 }
             }
 
             @Override
             public void onConfigChanged(ZenModeConfig config) {
-                if (mStackView != null) {
-                    mStackView.updateDots();
+                for (Bubble b : mBubbleData.getBubbles()) {
+                    b.setShowDot(b.showInShade(), true /* animate */);
                 }
             }
         });
@@ -465,7 +465,7 @@
      */
     public boolean isBubbleNotificationSuppressedFromShade(String key) {
         boolean isBubbleAndSuppressed = mBubbleData.hasBubbleWithKey(key)
-                && !mBubbleData.getBubbleWithKey(key).showInShadeWhenBubble();
+                && !mBubbleData.getBubbleWithKey(key).showInShade();
         NotificationEntry entry = mNotificationEntryManager.getActiveNotificationUnfiltered(key);
         String groupKey = entry != null ? entry.getSbn().getGroupKey() : null;
         boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey);
@@ -630,11 +630,8 @@
                 Bubble bubble = mBubbleData.getBubbleWithKey(key);
                 boolean bubbleExtended = entry != null && entry.isBubble() && userRemovedNotif;
                 if (bubbleExtended) {
-                    bubble.setShowInShadeWhenBubble(false);
-                    bubble.setShowBubbleDot(false);
-                    if (mStackView != null) {
-                        mStackView.updateDotVisibility(entry.getKey());
-                    }
+                    bubble.setShowInShade(false);
+                    bubble.setShowDot(false /* show */, true /* animate */);
                     mNotificationEntryManager.updateNotifications(
                             "BubbleController.onNotificationRemoveRequested");
                     return true;
@@ -660,11 +657,8 @@
                 // As far as group manager is concerned, once a child is no longer shown
                 // in the shade, it is essentially removed.
                 mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry());
-                bubbleChild.setShowInShadeWhenBubble(false);
-                bubbleChild.setShowBubbleDot(false);
-                if (mStackView != null) {
-                    mStackView.updateDotVisibility(bubbleChild.getKey());
-                }
+                bubbleChild.setShowInShade(false);
+                bubbleChild.setShowDot(false /* show */, true /* animate */);
             }
             // And since all children are removed, remove the summary.
             mNotificationGroupManager.onEntryRemoved(summary);
@@ -705,6 +699,10 @@
 
             if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
                     && (canLaunchInActivityView(mContext, entry) || wasAdjusted)) {
+                if (wasAdjusted && !previouslyUserCreated) {
+                    // Gotta treat the auto-bubbled / whitelisted packaged bubbles as usercreated
+                    mUserCreatedBubbles.add(entry.getKey());
+                }
                 updateBubble(entry);
             }
         }
@@ -721,6 +719,10 @@
                 // It was previously a bubble but no longer a bubble -- lets remove it
                 removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
             } else if (shouldBubble) {
+                if (wasAdjusted && !previouslyUserCreated) {
+                    // Gotta treat the auto-bubbled / whitelisted packaged bubbles as usercreated
+                    mUserCreatedBubbles.add(entry.getKey());
+                }
                 updateBubble(entry);
             }
         }
@@ -767,7 +769,7 @@
                 // If the bubble is removed for user switching, leave the notification in place.
                 if (reason != DISMISS_USER_CHANGED) {
                     if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
-                            && !bubble.showInShadeWhenBubble()) {
+                            && !bubble.showInShade()) {
                         // The bubble is gone & the notification is gone, time to actually remove it
                         mNotificationEntryManager.performRemoveNotification(
                                 bubble.getEntry().getSbn(), UNDEFINED_DISMISS_REASON);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 2ca993b..034bff3 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -210,8 +210,8 @@
             setSelectedBubbleInternal(bubble);
         }
         boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
-        bubble.setShowInShadeWhenBubble(!isBubbleExpandedAndSelected && showInShade);
-        bubble.setShowBubbleDot(!isBubbleExpandedAndSelected);
+        bubble.setShowInShade(!isBubbleExpandedAndSelected && showInShade);
+        bubble.setShowDot(!isBubbleExpandedAndSelected /* show */, true /* animate */);
         dispatchPendingChanges();
     }
 
@@ -303,9 +303,8 @@
         if (notif.getRanking().visuallyInterruptive()) {
             return true;
         }
-        final boolean suppressedFromShade = hasBubbleWithKey(notif.getKey())
-                && !getBubbleWithKey(notif.getKey()).showInShadeWhenBubble();
-        return suppressedFromShade;
+        return hasBubbleWithKey(notif.getKey())
+                && !getBubbleWithKey(notif.getKey()).showInShade();
     }
 
     private void doAdd(Bubble bubble) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 856b15e..efc955d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -603,7 +603,7 @@
                 action,
                 mStackView.getNormalizedXPosition(),
                 mStackView.getNormalizedYPosition(),
-                bubble.showInShadeWhenBubble(),
+                bubble.showInShade(),
                 bubble.isOngoing(),
                 false /* isAppForeground (unused) */);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
index fd7fff4..e138d93 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
@@ -66,6 +66,8 @@
     private static final String ALLOW_SHORTCUTS_TO_BUBBLE = "allow_shortcuts_to_bubble";
     private static final boolean ALLOW_SHORTCUT_TO_BUBBLE_DEFAULT = false;
 
+    private static final String WHITELISTED_AUTO_BUBBLE_APPS = "whitelisted_auto_bubble_apps";
+
     /**
      * When true, if a notification has the information necessary to bubble (i.e. valid
      * contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata}
@@ -103,6 +105,24 @@
     }
 
     /**
+     * Returns whether the provided package is whitelisted to bubble.
+     */
+    static boolean isPackageWhitelistedToAutoBubble(Context context, String packageName) {
+        String unsplitList = Settings.Secure.getString(context.getContentResolver(),
+                WHITELISTED_AUTO_BUBBLE_APPS);
+        if (unsplitList != null) {
+            // We expect the list to be separated by commas and no white space (but we trim in case)
+            String[] packageList = unsplitList.split(",");
+            for (int i = 0; i < packageList.length; i++) {
+                if (packageList[i].trim().equals(packageName)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
      * If {@link #allowAnyNotifToBubble(Context)} is true, this method creates and adds
      * {@link android.app.Notification.BubbleMetadata} to the notification entry as long as
      * the notification has necessary info for BubbleMetadata.
@@ -113,6 +133,8 @@
             boolean previouslyUserCreated) {
         Notification.BubbleMetadata metadata = null;
         boolean addedMetadata = false;
+        boolean whiteListedToAutoBubble =
+                isPackageWhitelistedToAutoBubble(context, entry.getSbn().getPackageName());
 
         Notification notification = entry.getSbn().getNotification();
         boolean isMessage = Notification.MessagingStyle.class.equals(
@@ -170,9 +192,9 @@
             }
         }
 
-        if (previouslyUserCreated && addedMetadata) {
-            // Update to a previous bubble, set its flag now so the update goes
-            // to the bubble.
+        boolean bubbleForWhitelist = whiteListedToAutoBubble && (addedMetadata || hasMetadata);
+        if ((previouslyUserCreated && addedMetadata) || bubbleForWhitelist) {
+            // Update to a previous bubble (or new autobubble), set its flag now.
             if (DEBUG_EXPERIMENTS) {
                 Log.d(TAG, "Setting FLAG_BUBBLE for: " + entry.getKey());
             }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
index 58f3f22..78e98eb 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
@@ -206,7 +206,7 @@
     void setupFlyoutStartingAsDot(
             CharSequence updateMessage, PointF stackPos, float parentWidth,
             boolean arrowPointingLeft, int dotColor, @Nullable Runnable onLayoutComplete,
-            @Nullable Runnable onHide, float[] dotCenter) {
+            @Nullable Runnable onHide, float[] dotCenter, boolean hideDot) {
         mArrowPointingLeft = arrowPointingLeft;
         mDotColor = dotColor;
         mOnHide = onHide;
@@ -242,12 +242,14 @@
 
             // Calculate the difference in size between the flyout and the 'dot' so that we can
             // transform into the dot later.
-            mFlyoutToDotWidthDelta = getWidth() - mNewDotSize;
-            mFlyoutToDotHeightDelta = getHeight() - mNewDotSize;
+            final float newDotSize = hideDot ? 0f : mNewDotSize;
+            mFlyoutToDotWidthDelta = getWidth() - newDotSize;
+            mFlyoutToDotHeightDelta = getHeight() - newDotSize;
 
             // Calculate the translation values needed to be in the correct 'new dot' position.
-            final float dotPositionX = stackPos.x + mDotCenter[0] - (mOriginalDotSize / 2f);
-            final float dotPositionY = stackPos.y + mDotCenter[1] - (mOriginalDotSize / 2f);
+            final float adjustmentForScaleAway = hideDot ? 0f : (mOriginalDotSize / 2f);
+            final float dotPositionX = stackPos.x + mDotCenter[0] - adjustmentForScaleAway;
+            final float dotPositionY = stackPos.y + mDotCenter[1] - adjustmentForScaleAway;
 
             final float distanceFromFlyoutLeftToDotCenterX = mRestingTranslationX - dotPositionX;
             final float distanceFromLayoutTopToDotCenterY = restingTranslationY - dotPositionY;
@@ -319,8 +321,7 @@
         // percentage.
         final float width = getWidth() - (mFlyoutToDotWidthDelta * mPercentTransitionedToDot);
         final float height = getHeight() - (mFlyoutToDotHeightDelta * mPercentTransitionedToDot);
-        final float interpolatedRadius = mNewDotRadius * mPercentTransitionedToDot
-                + mCornerRadius * (1 - mPercentTransitionedToDot);
+        final float interpolatedRadius = getInterpolatedRadius();
 
         // Translate the flyout background towards the collapsed 'dot' state.
         mBgTranslationX = mTranslationXWhenDot * mPercentTransitionedToDot;
@@ -387,8 +388,7 @@
         if (!mTriangleOutline.isEmpty()) {
             // Draw the rect into the outline as a path so we can merge the triangle path into it.
             final Path rectPath = new Path();
-            final float interpolatedRadius = mNewDotRadius * mPercentTransitionedToDot
-                    + mCornerRadius * (1 - mPercentTransitionedToDot);
+            final float interpolatedRadius = getInterpolatedRadius();
             rectPath.addRoundRect(mBgRect, interpolatedRadius,
                     interpolatedRadius, Path.Direction.CW);
             outline.setConvexPath(rectPath);
@@ -420,4 +420,9 @@
             outline.mPath.transform(outlineMatrix);
         }
     }
+
+    private float getInterpolatedRadius() {
+        return mNewDotRadius * mPercentTransitionedToDot
+                + mCornerRadius * (1 - mPercentTransitionedToDot);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
index a1c77c0..9ff033c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
@@ -16,8 +16,14 @@
 package com.android.systemui.bubbles;
 
 import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 
 import com.android.launcher3.icons.BaseIconFactory;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ShadowGenerator;
 import com.android.systemui.R;
 
 /**
@@ -26,13 +32,37 @@
  * so there is no need to manage a pool across multiple threads.
  */
 public class BubbleIconFactory extends BaseIconFactory {
+
     protected BubbleIconFactory(Context context) {
         super(context, context.getResources().getConfiguration().densityDpi,
                 context.getResources().getDimensionPixelSize(R.dimen.individual_bubble_size));
     }
 
-    public int getBadgeSize() {
+    int getBadgeSize() {
         return mContext.getResources().getDimensionPixelSize(
                 com.android.launcher3.icons.R.dimen.profile_badge_size);
     }
+
+    BitmapInfo getBadgedBitmap(Bubble b) {
+        Bitmap userBadgedBitmap = createIconBitmap(
+                b.getUserBadgedAppIcon(), 1f, getBadgeSize());
+
+        Canvas c = new Canvas();
+        ShadowGenerator shadowGenerator = new ShadowGenerator(getBadgeSize());
+        c.setBitmap(userBadgedBitmap);
+        shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c);
+        BitmapInfo bitmapInfo = createIconBitmap(userBadgedBitmap);
+        return bitmapInfo;
+    }
+
+    BitmapInfo getBubbleBitmap(Drawable bubble, BitmapInfo badge) {
+        BitmapInfo bubbleIconInfo = createBadgedIconBitmap(bubble,
+                null /* user */,
+                true /* shrinkNonAdaptiveIcons */);
+
+        badgeWithDrawable(bubbleIconInfo.icon,
+                new BitmapDrawable(mContext.getResources(), badge.icon));
+        return bubbleIconInfo;
+    }
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 4a1bbe4..29de2f0 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -19,6 +19,8 @@
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
+import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_DEFAULT;
+import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_SUPPRESSED_FOR_FLYOUT;
 import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
@@ -169,7 +171,7 @@
      * Callback to run after the flyout hides. Also called if a new flyout is shown before the
      * previous one animates out.
      */
-    private Runnable mFlyoutOnHide;
+    private Runnable mAfterFlyoutHidden;
 
     /** Layout change listener that moves the stack to the nearest valid position on rotation. */
     private OnLayoutChangeListener mOrientationChangedListener;
@@ -674,18 +676,6 @@
     }
 
     /**
-     * Updates the visibility of the 'dot' indicating an update on the bubble.
-     *
-     * @param key the {@link NotificationEntry#key} associated with the bubble.
-     */
-    public void updateDotVisibility(String key) {
-        Bubble b = mBubbleData.getBubbleWithKey(key);
-        if (b != null) {
-            b.updateDotVisibility();
-        }
-    }
-
-    /**
      * Sets the listener to notify when the bubble stack is expanded.
      */
     public void setExpandListener(BubbleController.BubbleExpandListener listener) {
@@ -707,9 +697,9 @@
     }
 
     /**
-     * The {@link BubbleView} that is expanded, null if one does not exist.
+     * The {@link BadgedImageView} that is expanded, null if one does not exist.
      */
-    BubbleView getExpandedBubbleView() {
+    BadgedImageView getExpandedBubbleView() {
         return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
     }
 
@@ -731,7 +721,7 @@
         Bubble bubbleToExpand = mBubbleData.getBubbleWithKey(key);
         if (bubbleToExpand != null) {
             setSelectedBubble(bubbleToExpand);
-            bubbleToExpand.setShowInShadeWhenBubble(false);
+            bubbleToExpand.setShowInShade(false);
             setExpanded(true);
         }
     }
@@ -746,8 +736,8 @@
             mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
         }
 
+        bubble.setBubbleIconFactory(mBubbleIconFactory);
         bubble.inflate(mInflater, this);
-        bubble.getIconView().setBubbleIconFactory(mBubbleIconFactory);
         bubble.getIconView().updateViews();
 
         // Set the dot position to the opposite of the side the stack is resting on, since the stack
@@ -884,7 +874,7 @@
             if (isIntersecting(mBubbleContainer, x, y)) {
                 // Could be tapping or dragging a bubble while expanded
                 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
-                    BubbleView view = (BubbleView) mBubbleContainer.getChildAt(i);
+                    BadgedImageView view = (BadgedImageView) mBubbleContainer.getChildAt(i);
                     if (isIntersecting(view, x, y)) {
                         return view;
                     }
@@ -1028,9 +1018,9 @@
     }
 
     /** Return the BubbleView at the given index from the bubble container. */
-    public BubbleView getBubbleAt(int i) {
+    public BadgedImageView getBubbleAt(int i) {
         return mBubbleContainer.getChildCount() > i
-                ? (BubbleView) mBubbleContainer.getChildAt(i)
+                ? (BadgedImageView) mBubbleContainer.getChildAt(i)
                 : null;
     }
 
@@ -1382,16 +1372,6 @@
                         : 0f);
     }
 
-    /** Updates the dot visibility, this is used in response to a zen mode config change. */
-    void updateDots() {
-        int bubbsCount = mBubbleContainer.getChildCount();
-        for (int i = 0; i < bubbsCount; i++) {
-            BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
-            // If nothing changed the animation won't happen
-            bv.updateDotVisibility(true /* animate */);
-        }
-    }
-
     /**
      * Calculates the y position of the expanded view when it is expanded.
      */
@@ -1405,37 +1385,40 @@
     @VisibleForTesting
     void animateInFlyoutForBubble(Bubble bubble) {
         final CharSequence updateMessage = bubble.getUpdateMessage(getContext());
-        if (!bubble.showFlyoutForBubble()) {
-            // In case flyout was suppressed for this update, reset now.
-            bubble.setSuppressFlyout(false);
-            return;
-        }
+        final BadgedImageView bubbleView = bubble.getIconView();
         if (updateMessage == null
+                || !bubble.showFlyout()
                 || isExpanded()
                 || mIsExpansionAnimating
                 || mIsGestureInProgress
                 || mBubbleToExpandAfterFlyoutCollapse != null
-                || bubble.getIconView() == null) {
+                || bubbleView == null) {
+            if (bubbleView != null) {
+                bubbleView.setDotState(DOT_STATE_DEFAULT);
+            }
             // Skip the message if none exists, we're expanded or animating expansion, or we're
             // about to expand a bubble from the previous tapped flyout, or if bubble view is null.
             return;
         }
+
         mFlyoutDragDeltaX = 0f;
         clearFlyoutOnHide();
-        mFlyoutOnHide = () -> {
-            resetDot(bubble);
-            if (mBubbleToExpandAfterFlyoutCollapse == null) {
-                return;
+        mAfterFlyoutHidden = () -> {
+            // Null it out to ensure it runs once.
+            mAfterFlyoutHidden = null;
+
+            if (mBubbleToExpandAfterFlyoutCollapse != null) {
+                // User tapped on the flyout and we should expand
+                mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
+                mBubbleData.setExpanded(true);
+                mBubbleToExpandAfterFlyoutCollapse = null;
             }
-            mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
-            mBubbleData.setExpanded(true);
-            mBubbleToExpandAfterFlyoutCollapse = null;
+            bubbleView.setDotState(DOT_STATE_DEFAULT);
         };
         mFlyout.setVisibility(INVISIBLE);
 
-        // Temporarily suppress the dot while the flyout is visible.
-        bubble.getIconView().setSuppressDot(
-                true /* suppressDot */, false /* animate */);
+        // Don't show the dot when we're animating the flyout
+        bubbleView.setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
 
         // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
         post(() -> {
@@ -1461,8 +1444,9 @@
                     mStackAnimationController.isStackOnLeftSide(),
                     bubble.getIconView().getDotColor() /* dotColor */,
                     expandFlyoutAfterDelay /* onLayoutComplete */,
-                    mFlyoutOnHide,
-                    bubble.getIconView().getDotCenter());
+                    mAfterFlyoutHidden,
+                    bubble.getIconView().getDotCenter(),
+                    !bubble.showDot());
             mFlyout.bringToFront();
         });
         mFlyout.removeCallbacks(mHideFlyout);
@@ -1470,24 +1454,6 @@
         logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
     }
 
-    private void resetDot(Bubble bubble) {
-        final boolean suppressDot = !bubble.showBubbleDot();
-        // If we're going to suppress the dot, make it visible first so it'll
-        // visibly animate away.
-
-        if (suppressDot) {
-            bubble.getIconView().setSuppressDot(
-                    false /* suppressDot */, false /* animate */);
-        }
-        // Reset dot suppression. If we're not suppressing due to DND, then
-        // stop suppressing it with no animation (since the flyout has
-        // transformed into the dot). If we are suppressing due to DND, animate
-        // it away.
-        bubble.getIconView().setSuppressDot(
-                suppressDot /* suppressDot */,
-                suppressDot /* animate */);
-    }
-
     /** Hide the flyout immediately and cancel any pending hide runnables. */
     private void hideFlyoutImmediate() {
         clearFlyoutOnHide();
@@ -1498,11 +1464,11 @@
 
     private void clearFlyoutOnHide() {
         mFlyout.removeCallbacks(mAnimateInFlyout);
-        if (mFlyoutOnHide == null) {
+        if (mAfterFlyoutHidden == null) {
             return;
         }
-        mFlyoutOnHide.run();
-        mFlyoutOnHide = null;
+        mAfterFlyoutHidden.run();
+        mAfterFlyoutHidden = null;
     }
 
     @Override
@@ -1599,8 +1565,7 @@
     private void updateBubbleZOrdersAndDotPosition(boolean animate) {
         int bubbleCount = mBubbleContainer.getChildCount();
         for (int i = 0; i < bubbleCount; i++) {
-            BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
-            bv.updateDotVisibility(true /* animate */);
+            BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
             bv.setZ((mMaxBubbles * mBubbleElevation) - i);
             // If the dot is on the left, and so is the stack, we need to change the dot position.
             if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
@@ -1705,7 +1670,7 @@
                     action,
                     getNormalizedXPosition(),
                     getNormalizedYPosition(),
-                    bubble.showInShadeWhenBubble(),
+                    bubble.showInShade(),
                     bubble.isOngoing(),
                     false /* isAppForeground (unused) */);
         }
@@ -1727,8 +1692,8 @@
         List<Bubble> bubbles = new ArrayList<>();
         for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
             View child = mBubbleContainer.getChildAt(i);
-            if (child instanceof BubbleView) {
-                String key = ((BubbleView) child).getKey();
+            if (child instanceof BadgedImageView) {
+                String key = ((BadgedImageView) child).getKey();
                 Bubble bubble = mBubbleData.getBubbleWithKey(key);
                 bubbles.add(bubble);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index 4240e06..44e013a 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -95,7 +95,7 @@
             return false;
         }
 
-        if (!(mTouchedView instanceof BubbleView)
+        if (!(mTouchedView instanceof BadgedImageView)
                 && !(mTouchedView instanceof BubbleStackView)
                 && !(mTouchedView instanceof BubbleFlyoutView)) {
             // Not touching anything touchable, but we shouldn't collapse (e.g. touching edge
@@ -187,7 +187,7 @@
                     mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
                 } else if (shouldDismiss) {
                     final String individualBubbleKey =
-                            isStack ? null : ((BubbleView) mTouchedView).getKey();
+                            isStack ? null : ((BadgedImageView) mTouchedView).getKey();
                     mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
                             () -> {
                                 if (isStack) {
@@ -214,7 +214,7 @@
                     // Toggle expansion
                     mBubbleData.setExpanded(!mBubbleData.isExpanded());
                 } else {
-                    final String key = ((BubbleView) mTouchedView).getKey();
+                    final String key = ((BadgedImageView) mTouchedView).getKey();
                     mBubbleData.setSelectedBubble(mBubbleData.getBubbleWithKey(key));
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java
deleted file mode 100644
index 79807b3..0000000
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.bubbles;
-
-import android.annotation.Nullable;
-import android.app.Notification;
-import android.content.Context;
-import android.content.pm.LauncherApps;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Matrix;
-import android.graphics.Path;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-import android.util.AttributeSet;
-import android.util.PathParser;
-import android.widget.FrameLayout;
-
-import com.android.internal.graphics.ColorUtils;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.ColorExtractor;
-import com.android.launcher3.icons.ShadowGenerator;
-import com.android.systemui.Interpolators;
-import com.android.systemui.R;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-
-/**
- * A floating object on the screen that can post message updates.
- */
-public class BubbleView extends FrameLayout {
-
-    // Same value as Launcher3 badge code
-    private static final float WHITE_SCRIM_ALPHA = 0.54f;
-    private Context mContext;
-
-    private BadgedImageView mBadgedImageView;
-    private int mDotColor;
-    private ColorExtractor mColorExtractor;
-
-    // mBubbleIconFactory cannot be static because it depends on Context.
-    private BubbleIconFactory mBubbleIconFactory;
-
-    private boolean mSuppressDot;
-
-    private Bubble mBubble;
-
-    public BubbleView(Context context) {
-        this(context, null);
-    }
-
-    public BubbleView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        mContext = context;
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mBadgedImageView = findViewById(R.id.bubble_image);
-        mColorExtractor = new ColorExtractor();
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-    }
-
-    /**
-     * Populates this view with a bubble.
-     * <p>
-     * This should only be called when a new bubble is being set on the view, updates to the
-     * current bubble should use {@link #update(Bubble)}.
-     *
-     * @param bubble the bubble to display in this view.
-     */
-    public void setBubble(Bubble bubble) {
-        mBubble = bubble;
-    }
-
-    /**
-     * @param factory Factory for creating normalized bubble icons.
-     */
-    public void setBubbleIconFactory(BubbleIconFactory factory) {
-        mBubbleIconFactory = factory;
-    }
-
-    /**
-     * The {@link NotificationEntry} associated with this view, if one exists.
-     */
-    @Nullable
-    public NotificationEntry getEntry() {
-        return mBubble != null ? mBubble.getEntry() : null;
-    }
-
-    /**
-     * The key for the {@link NotificationEntry} associated with this view, if one exists.
-     */
-    @Nullable
-    public String getKey() {
-        return (mBubble != null) ? mBubble.getKey() : null;
-    }
-
-    /**
-     * Updates the UI based on the bubble, updates badge and animates messages as needed.
-     */
-    public void update(Bubble bubble) {
-        mBubble = bubble;
-        updateViews();
-    }
-
-    /** Changes the dot's visibility to match the bubble view's state. */
-    void updateDotVisibility(boolean animate) {
-        updateDotVisibility(animate, null /* after */);
-    }
-
-    /**
-     * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the
-     * flyout is visible or animating, to hide the dot until the flyout visually transforms into it.
-     */
-    void setSuppressDot(boolean suppressDot, boolean animate) {
-        mSuppressDot = suppressDot;
-        updateDotVisibility(animate);
-    }
-
-    boolean isDotShowing() {
-        return mBubble.showBubbleDot() && !mSuppressDot;
-    }
-
-    int getDotColor() {
-        return mDotColor;
-    }
-
-    /** Sets the position of the 'new' dot, animating it out and back in if requested. */
-    void setDotPosition(boolean onLeft, boolean animate) {
-        if (animate && onLeft != mBadgedImageView.getDotOnLeft() && isDotShowing()) {
-            animateDot(false /* showDot */, () -> {
-                mBadgedImageView.setDotOnLeft(onLeft);
-                animateDot(true /* showDot */, null);
-            });
-        } else {
-            mBadgedImageView.setDotOnLeft(onLeft);
-        }
-    }
-
-    float[] getDotCenter() {
-        float[] unscaled = mBadgedImageView.getDotCenter();
-        return new float[]{unscaled[0], unscaled[1]};
-    }
-
-    boolean getDotPositionOnLeft() {
-        return mBadgedImageView.getDotOnLeft();
-    }
-
-    /**
-     * Changes the dot's visibility to match the bubble view's state, running the provided callback
-     * after animation if requested.
-     */
-    private void updateDotVisibility(boolean animate, Runnable after) {
-        final boolean showDot = isDotShowing();
-        if (animate) {
-            animateDot(showDot, after);
-        } else {
-            mBadgedImageView.setShowDot(showDot);
-            mBadgedImageView.setDotScale(showDot ? 1f : 0f);
-        }
-    }
-
-    /**
-     * Animates the badge to show or hide.
-     */
-    private void animateDot(boolean showDot, Runnable after) {
-        if (mBadgedImageView.isShowingDot() == showDot) {
-            return;
-        }
-        // Do NOT wait until after animation ends to setShowDot
-        // to avoid overriding more recent showDot states.
-        mBadgedImageView.setShowDot(showDot);
-        mBadgedImageView.clearAnimation();
-        mBadgedImageView.animate().setDuration(200)
-                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                .setUpdateListener((valueAnimator) -> {
-                    float fraction = valueAnimator.getAnimatedFraction();
-                    fraction = showDot ? fraction : 1f - fraction;
-                    mBadgedImageView.setDotScale(fraction);
-                }).withEndAction(() -> {
-            mBadgedImageView.setDotScale(showDot ? 1f : 0f);
-            if (after != null) {
-                after.run();
-            }
-        }).start();
-    }
-
-    void updateViews() {
-        if (mBubble == null || mBubbleIconFactory == null) {
-            return;
-        }
-
-        Drawable bubbleDrawable = getBubbleDrawable(mContext);
-        BitmapInfo badgeBitmapInfo = getBadgedBitmap();
-        BitmapInfo bubbleBitmapInfo = getBubbleBitmap(bubbleDrawable, badgeBitmapInfo);
-        mBadgedImageView.setImageBitmap(bubbleBitmapInfo.icon);
-
-        // Update badge.
-        mDotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, Color.WHITE, WHITE_SCRIM_ALPHA);
-        mBadgedImageView.setDotColor(mDotColor);
-
-        // Update dot.
-        Path iconPath = PathParser.createPathFromPathData(
-                getResources().getString(com.android.internal.R.string.config_icon_mask));
-        Matrix matrix = new Matrix();
-        float scale = mBubbleIconFactory.getNormalizer().getScale(bubbleDrawable,
-                null /* outBounds */, null /* path */, null /* outMaskShape */);
-        float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f;
-        matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
-                radius /* pivot y */);
-        iconPath.transform(matrix);
-        mBadgedImageView.drawDot(iconPath);
-
-        animateDot(isDotShowing(), null /* after */);
-    }
-
-    Drawable getBubbleDrawable(Context context) {
-        if (mBubble.getShortcutInfo() != null && mBubble.usingShortcutInfo()) {
-            LauncherApps launcherApps =
-                    (LauncherApps) getContext().getSystemService(Context.LAUNCHER_APPS_SERVICE);
-            int density = getContext().getResources().getConfiguration().densityDpi;
-            return launcherApps.getShortcutIconDrawable(mBubble.getShortcutInfo(), density);
-        } else {
-            Notification.BubbleMetadata metadata = getEntry().getBubbleMetadata();
-            Icon ic = metadata.getIcon();
-            return ic.loadDrawable(context);
-        }
-    }
-
-    BitmapInfo getBadgedBitmap() {
-        Bitmap userBadgedBitmap = mBubbleIconFactory.createIconBitmap(
-                mBubble.getUserBadgedAppIcon(), 1f, mBubbleIconFactory.getBadgeSize());
-
-        Canvas c = new Canvas();
-        ShadowGenerator shadowGenerator = new ShadowGenerator(mBubbleIconFactory.getBadgeSize());
-        c.setBitmap(userBadgedBitmap);
-        shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c);
-        BitmapInfo bitmapInfo = mBubbleIconFactory.createIconBitmap(userBadgedBitmap);
-        return bitmapInfo;
-    }
-
-    BitmapInfo getBubbleBitmap(Drawable bubble, BitmapInfo badge) {
-        BitmapInfo bubbleIconInfo = mBubbleIconFactory.createBadgedIconBitmap(bubble,
-                null /* user */,
-                true /* shrinkNonAdaptiveIcons */);
-
-        mBubbleIconFactory.badgeWithDrawable(bubbleIconInfo.icon,
-                new BitmapDrawable(mContext.getResources(), badge.icon));
-        return bubbleIconInfo;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
index fedd855..5041354 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -804,8 +804,8 @@
             List<Notification.Action> actions = smartActionsFuture.get(timeoutMs,
                     TimeUnit.MILLISECONDS);
             long waitTimeMs = SystemClock.uptimeMillis() - startTimeMs;
-            Slog.d(TAG, String.format("Wait time for smart actions: %d ms",
-                    waitTimeMs));
+            Slog.d(TAG, String.format("Got %d smart actions. Wait time: %d ms",
+                    actions.size(), waitTimeMs));
             notifyScreenshotOp(screenshotId, smartActionsProvider,
                     ScreenshotNotificationSmartActionsProvider.ScreenshotOp.WAIT_FOR_SMART_ACTIONS,
                     ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.SUCCESS,
@@ -813,7 +813,8 @@
             return actions;
         } catch (Throwable e) {
             long waitTimeMs = SystemClock.uptimeMillis() - startTimeMs;
-            Slog.d(TAG, "Failed to obtain screenshot notification smart actions.", e);
+            Slog.e(TAG, String.format("Error getting smart actions. Wait time: %d ms", waitTimeMs),
+                    e);
             ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus status =
                     (e instanceof TimeoutException)
                             ? ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.TIMEOUT
@@ -942,14 +943,16 @@
     public static class SmartActionsReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
-            PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
+            PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
+            Intent actionIntent = pendingIntent.getIntent();
+            String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE);
+            Slog.d(TAG, "Executing smart action [" + actionType + "]:" + actionIntent);
             ActivityOptions opts = ActivityOptions.makeBasic();
-            context.startActivityAsUser(actionIntent.getIntent(), opts.toBundle(),
+            context.startActivityAsUser(actionIntent, opts.toBundle(),
                     UserHandle.CURRENT);
 
-            Slog.d(TAG, "Screenshot notification smart action is invoked.");
             notifyScreenshotAction(context, intent.getStringExtra(EXTRA_ID),
-                    intent.getStringExtra(EXTRA_ACTION_TYPE),
+                    actionType,
                     true);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index d2268e1..76925b4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -456,9 +456,9 @@
                     SystemUiDeviceConfigFlags
                             .SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS,
                     1000);
-            List<Notification.Action> smartActions = buildSmartActions(
-                    GlobalScreenshot.getSmartActions(mScreenshotId, smartActionsFuture,
-                            timeoutMs, mSmartActionsProvider), context);
+            List<Notification.Action> smartActions = GlobalScreenshot.getSmartActions(mScreenshotId,
+                    smartActionsFuture, timeoutMs, mSmartActionsProvider);
+            smartActions = buildSmartActions(smartActions, context);
             for (Notification.Action action : smartActions) {
                 notificationBuilder.addAction(action);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 5c71a57..5575d10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -343,6 +343,7 @@
     private BiometricUnlockController mBiometricUnlockController;
     private final LightBarController mLightBarController;
     private final Lazy<LockscreenWallpaper> mLockscreenWallpaperLazy;
+    @Nullable
     protected LockscreenWallpaper mLockscreenWallpaper;
     private final AutoHideController mAutoHideController;
     @Nullable
@@ -1059,7 +1060,7 @@
 
         createNavigationBar(result);
 
-        if (ENABLE_LOCKSCREEN_WALLPAPER) {
+        if (ENABLE_LOCKSCREEN_WALLPAPER && mWallpaperSupported) {
             mLockscreenWallpaper = mLockscreenWallpaperLazy.get();
             mLockscreenWallpaper.setHandler(mHandler);
         }
@@ -2807,7 +2808,9 @@
 
     @Override
     public void setLockscreenUser(int newUserId) {
-        mLockscreenWallpaper.setCurrentUser(newUserId);
+        if (mLockscreenWallpaper != null) {
+            mLockscreenWallpaper.setCurrentUser(newUserId);
+        }
         mScrimController.setCurrentUser(newUserId);
         if (mWallpaperSupported) {
             mWallpaperChangedReceiver.onReceive(mContext, null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 8c9f759..4c707f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -157,9 +157,14 @@
     private SuperStatusBarViewFactory mSuperStatusBarViewFactory;
     private BubbleData mBubbleData;
 
+    private TestableLooper mTestableLooper;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+
+        mTestableLooper = TestableLooper.get(this);
+
         mContext.addMockSystemService(FaceManager.class, mFaceManager);
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
 
@@ -262,7 +267,7 @@
                 mRow.getEntry().getKey()));
 
         // Make it look like dismissed notif
-        mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).setShowInShadeWhenBubble(false);
+        mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).setShowInShade(false);
 
         // Now remove the bubble
         mBubbleController.removeBubble(
@@ -346,14 +351,14 @@
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
 
         // Last added is the one that is expanded
-        assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry());
+        assertEquals(mRow2.getEntry(), mBubbleData.getSelectedBubble().getEntry());
         assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow2.getEntry().getKey()));
 
         // Switch which bubble is expanded
         mBubbleController.selectBubble(mRow.getEntry().getKey());
         stackView.setExpandedBubble(mRow.getEntry().getKey());
-        assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry());
+        assertEquals(mRow.getEntry(), stackView.getExpandedBubble().getEntry());
         assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow.getEntry().getKey()));
 
@@ -377,7 +382,9 @@
         assertTrue(mBubbleController.hasBubbles());
         assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow.getEntry().getKey()));
-        assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+
+        mTestableLooper.processAllMessages();
+        assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
 
         // Expand
         mBubbleController.expandStack();
@@ -388,7 +395,7 @@
         assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow.getEntry().getKey()));
         // Notif shouldn't show dot after expansion
-        assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+        assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
     }
 
     @Test
@@ -401,10 +408,11 @@
         assertTrue(mBubbleController.hasBubbles());
         assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow.getEntry().getKey()));
-        assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+
+        mTestableLooper.processAllMessages();
+        assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
 
         // Expand
-        BubbleStackView stackView = mBubbleController.getStackView();
         mBubbleController.expandStack();
         assertTrue(mBubbleController.isStackExpanded());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
@@ -413,7 +421,7 @@
         assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow.getEntry().getKey()));
         // Notif shouldn't show dot after expansion
-        assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+        assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
 
         // Send update
         mEntryListener.onPreEntryUpdated(mRow.getEntry());
@@ -423,7 +431,7 @@
         assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow.getEntry().getKey()));
         // Notif shouldn't show dot after expansion
-        assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+        assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
     }
 
     @Test
@@ -443,7 +451,7 @@
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
 
         // Last added is the one that is expanded
-        assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry());
+        assertEquals(mRow2.getEntry(), stackView.getExpandedBubble().getEntry());
         assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow2.getEntry().getKey()));
 
@@ -453,7 +461,7 @@
         verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
 
         // Make sure first bubble is selected
-        assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry());
+        assertEquals(mRow.getEntry(), stackView.getExpandedBubble().getEntry());
         verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
 
         // Dismiss that one
@@ -555,7 +563,9 @@
         mEntryListener.onPendingEntryAdded(mRow.getEntry());
         assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow.getEntry().getKey()));
-        assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+
+        mTestableLooper.processAllMessages();
+        assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
index a9be30b..95c7af3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -183,7 +183,7 @@
         // Verify
         verifyUpdateReceived();
         BubbleData.Update update = mUpdateCaptor.getValue();
-        assertThat(update.addedBubble.showFlyoutForBubble()).isFalse();
+        assertThat(update.addedBubble.showFlyout()).isFalse();
     }
 
     @Test
@@ -199,7 +199,7 @@
         // Verify
         verifyUpdateReceived();
         BubbleData.Update update = mUpdateCaptor.getValue();
-        assertThat(update.addedBubble.showFlyoutForBubble()).isTrue();
+        assertThat(update.addedBubble.showFlyout()).isTrue();
     }
 
     @Test
@@ -218,7 +218,7 @@
 
         // Verify
         BubbleData.Update update = mUpdateCaptor.getValue();
-        assertThat(update.updatedBubble.showFlyoutForBubble()).isFalse();
+        assertThat(update.updatedBubble.showFlyout()).isFalse();
     }
 
     @Test
@@ -239,7 +239,7 @@
 
         // Verify
         BubbleData.Update update = mUpdateCaptor.getValue();
-        assertThat(update.updatedBubble.showFlyoutForBubble()).isTrue();
+        assertThat(update.updatedBubble.showFlyout()).isTrue();
     }
 
     // COLLAPSED / ADD
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
index a8961a8..376ecf7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
@@ -60,7 +60,8 @@
     @Test
     public void testShowFlyout_isVisible() {
         mFlyout.setupFlyoutStartingAsDot(
-                "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter);
+                "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
+                false);
         mFlyout.setVisibility(View.VISIBLE);
 
         assertEquals("Hello", mFlyoutText.getText());
@@ -71,7 +72,8 @@
     public void testFlyoutHide_runsCallback() {
         Runnable after = Mockito.mock(Runnable.class);
         mFlyout.setupFlyoutStartingAsDot(
-                "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter);
+                "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter,
+                false);
         mFlyout.hideFlyout();
 
         verify(after).run();
@@ -80,7 +82,8 @@
     @Test
     public void testSetCollapsePercent() {
         mFlyout.setupFlyoutStartingAsDot(
-                "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter);
+                "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
+                false);
         mFlyout.setVisibility(View.VISIBLE);
 
         mFlyout.setCollapsePercent(1f);
diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/PinnerService.java
index d5f7956..135f6f3 100644
--- a/services/core/java/com/android/server/PinnerService.java
+++ b/services/core/java/com/android/server/PinnerService.java
@@ -45,6 +45,7 @@
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.DeviceConfig;
 import android.provider.MediaStore;
 import android.provider.Settings;
 import android.system.ErrnoException;
@@ -97,9 +98,12 @@
     private static final int KEY_HOME = 1;
     private static final int KEY_ASSISTANT = 2;
 
-    // Pin the camera application.
-    private static boolean PROP_PIN_CAMERA = SystemProperties.getBoolean(
-            "pinner.pin_camera", true);
+    // Pin the camera application. Default to the system property only if the experiment phenotype
+    // property is not set.
+    private static boolean PROP_PIN_CAMERA =
+            DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT,
+                                    "pin_camera",
+                                    SystemProperties.getBoolean("pinner.pin_camera", true));
     // Pin using pinlist.meta when pinning apps.
     private static boolean PROP_PIN_PINLIST = SystemProperties.getBoolean(
             "pinner.use_pinlist", true);
diff --git a/services/core/java/com/android/server/integrity/model/BitInputStream.java b/services/core/java/com/android/server/integrity/model/BitInputStream.java
new file mode 100644
index 0000000..09bc7e8
--- /dev/null
+++ b/services/core/java/com/android/server/integrity/model/BitInputStream.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.integrity.model;
+
+/** A wrapper class for reading a stream of bits. */
+public class BitInputStream {
+
+    private byte[] mRuleBytes;
+    private long mBitPointer;
+
+    public BitInputStream(byte[] ruleBytes) {
+        this.mRuleBytes = ruleBytes;
+        this.mBitPointer = 0;
+    }
+
+    /**
+     * Read the next number of bits from the stream.
+     *
+     * @param numOfBits The number of bits to read.
+     * @return The value read from the stream.
+     */
+    public int getNext(int numOfBits) {
+        int component = 0;
+        int count = 0;
+
+        int idx = (int) (mBitPointer / 8);
+        int offset = 7 - (int) (mBitPointer % 8);
+
+        while (count++ < numOfBits) {
+            if (idx >= mRuleBytes.length) {
+                throw new IllegalArgumentException(String.format("Invalid byte index: %d", idx));
+            }
+
+            component <<= 1;
+            component |= (mRuleBytes[idx] >>> offset) & 1;
+
+            offset--;
+            if (offset == -1) {
+                idx++;
+                offset = 7;
+            }
+        }
+
+        mBitPointer += numOfBits;
+        return component;
+    }
+
+    /** Check if there are bits left in the stream. */
+    public boolean hasNext() {
+        return mBitPointer / 8 < mRuleBytes.length;
+    }
+}
diff --git a/services/core/java/com/android/server/integrity/model/BitOutputStream.java b/services/core/java/com/android/server/integrity/model/BitOutputStream.java
new file mode 100644
index 0000000..ecb9189
--- /dev/null
+++ b/services/core/java/com/android/server/integrity/model/BitOutputStream.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.integrity.model;
+
+import java.util.BitSet;
+
+/** A wrapper class for writing a stream of bits. */
+public class BitOutputStream {
+
+    private BitSet mBitSet;
+    private int mIndex;
+
+    public BitOutputStream() {
+        mBitSet = new BitSet();
+        mIndex = 0;
+    }
+
+    /**
+     * Set the next number of bits in the stream to value.
+     *
+     * @param numOfBits The number of bits used to represent the value.
+     * @param value The value to convert to bits.
+     */
+    public void setNext(int numOfBits, int value) {
+        if (numOfBits <= 0) {
+            return;
+        }
+        int offset = 1 << (numOfBits - 1);
+        while (numOfBits-- > 0) {
+            mBitSet.set(mIndex, (value & offset) != 0);
+            offset >>= 1;
+            mIndex++;
+        }
+    }
+
+    /**
+     * Set the next bit in the stream to value.
+     *
+     * @param value The value to set the bit to.
+     */
+    public void setNext(boolean value) {
+        mBitSet.set(mIndex, value);
+        mIndex++;
+    }
+
+    /** Set the next bit in the stream to true. */
+    public void setNext() {
+        setNext(/* value= */ true);
+    }
+
+    /** Convert BitSet in big-endian to ByteArray in big-endian. */
+    public byte[] toByteArray() {
+        int bitSetSize = mBitSet.length();
+        int numOfBytes = bitSetSize / 8;
+        if (bitSetSize % 8 != 0) {
+            numOfBytes++;
+        }
+        byte[] bytes = new byte[numOfBytes];
+        for (int i = 0; i < mBitSet.length(); i++) {
+            if (mBitSet.get(i)) {
+                bytes[i / 8] |= 1 << (7 - (i % 8));
+            }
+        }
+        return bytes;
+    }
+
+    /** Clear the stream. */
+    public void clear() {
+        mBitSet.clear();
+        mIndex = 0;
+    }
+}
diff --git a/services/core/java/com/android/server/integrity/model/ComponentBitSize.java b/services/core/java/com/android/server/integrity/model/ComponentBitSize.java
new file mode 100644
index 0000000..d47ce2d
--- /dev/null
+++ b/services/core/java/com/android/server/integrity/model/ComponentBitSize.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.integrity.model;
+
+import android.content.integrity.Rule;
+
+/**
+ * A helper class containing information about the binary representation of different {@link Rule}
+ * components.
+ */
+public final class ComponentBitSize {
+    public static final int FORMAT_VERSION_BITS = 5;
+    public static final int EFFECT_BITS = 3;
+    public static final int KEY_BITS = 4;
+    public static final int OPERATOR_BITS = 3;
+    public static final int CONNECTOR_BITS = 2;
+    public static final int SEPARATOR_BITS = 2;
+    public static final int VALUE_SIZE_BITS = 5;
+    public static final int IS_HASHED_BITS = 1;
+
+    public static final int ATOMIC_FORMULA_START = 0;
+    public static final int COMPOUND_FORMULA_START = 1;
+    public static final int COMPOUND_FORMULA_END = 2;
+
+    public static final int DEFAULT_FORMAT_VERSION = 1;
+    public static final int SIGNAL_BIT = 1;
+}
diff --git a/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java b/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java
index aad177e..8aa0751 100644
--- a/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java
+++ b/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java
@@ -16,23 +16,132 @@
 
 package com.android.server.integrity.parser;
 
+import static com.android.server.integrity.model.ComponentBitSize.ATOMIC_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_END;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.CONNECTOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.EFFECT_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.FORMAT_VERSION_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.IS_HASHED_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.KEY_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.OPERATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.SEPARATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.SIGNAL_BIT;
+import static com.android.server.integrity.model.ComponentBitSize.VALUE_SIZE_BITS;
+
+import android.content.integrity.AtomicFormula;
+import android.content.integrity.CompoundFormula;
+import android.content.integrity.Formula;
 import android.content.integrity.Rule;
 
+import com.android.server.integrity.model.BitInputStream;
+
 import java.io.InputStream;
+import java.util.ArrayList;
 import java.util.List;
 
 /** A helper class to parse rules into the {@link Rule} model from Binary representation. */
 public class RuleBinaryParser implements RuleParser {
 
     @Override
-    public List<Rule> parse(byte[] ruleBytes) {
-        // TODO: Implement binary text parser.
-        return null;
+    public List<Rule> parse(byte[] ruleBytes) throws RuleParseException {
+        try {
+            BitInputStream bitInputStream = new BitInputStream(ruleBytes);
+            return parseRules(bitInputStream);
+        } catch (Exception e) {
+            throw new RuleParseException(e.getMessage(), e);
+        }
     }
 
     @Override
-    public List<Rule> parse(InputStream inputStream) {
-        // TODO: Implement stream parser.
-        return null;
+    public List<Rule> parse(InputStream inputStream) throws RuleParseException {
+        try {
+            byte[] ruleBytes = new byte[inputStream.available()];
+            inputStream.read(ruleBytes);
+            return parse(ruleBytes);
+        } catch (Exception e) {
+            throw new RuleParseException(e.getMessage(), e);
+        }
+    }
+
+    private List<Rule> parseRules(BitInputStream bitInputStream) {
+        List<Rule> parsedRules = new ArrayList<>();
+
+        // Read the rule binary file format version.
+        bitInputStream.getNext(FORMAT_VERSION_BITS);
+
+        while (bitInputStream.hasNext()) {
+            if (bitInputStream.getNext(SIGNAL_BIT) == 1) {
+                parsedRules.add(parseRule(bitInputStream));
+            }
+        }
+
+        return parsedRules;
+    }
+
+    private Rule parseRule(BitInputStream bitInputStream) {
+        Formula formula = parseFormula(bitInputStream);
+        int effect = bitInputStream.getNext(EFFECT_BITS);
+
+        if (bitInputStream.getNext(SIGNAL_BIT) != 1) {
+            throw new IllegalArgumentException("A rule must end with a '1' bit.");
+        }
+
+        return new Rule(formula, effect);
+    }
+
+    private Formula parseFormula(BitInputStream bitInputStream) {
+        int separator = bitInputStream.getNext(SEPARATOR_BITS);
+        switch (separator) {
+            case ATOMIC_FORMULA_START:
+                return parseAtomicFormula(bitInputStream);
+            case COMPOUND_FORMULA_START:
+                return parseCompoundFormula(bitInputStream);
+            case COMPOUND_FORMULA_END:
+                return null;
+            default:
+                throw new IllegalArgumentException(
+                        String.format("Unknown formula separator: %s", separator));
+        }
+    }
+
+    private CompoundFormula parseCompoundFormula(BitInputStream bitInputStream) {
+        int connector = bitInputStream.getNext(CONNECTOR_BITS);
+        List<Formula> formulas = new ArrayList<>();
+
+        Formula parsedFormula = parseFormula(bitInputStream);
+        while (parsedFormula != null) {
+            formulas.add(parsedFormula);
+            parsedFormula = parseFormula(bitInputStream);
+        }
+
+        return new CompoundFormula(connector, formulas);
+    }
+
+    private AtomicFormula parseAtomicFormula(BitInputStream bitInputStream) {
+        int key = bitInputStream.getNext(KEY_BITS);
+        int operator = bitInputStream.getNext(OPERATOR_BITS);
+
+        boolean isHashedValue = bitInputStream.getNext(IS_HASHED_BITS) == 1;
+        int valueSize = bitInputStream.getNext(VALUE_SIZE_BITS);
+        StringBuilder value = new StringBuilder();
+        while (valueSize-- > 0) {
+            value.append((char) bitInputStream.getNext(/* numOfBits= */ 8));
+        }
+
+        switch (key) {
+            case AtomicFormula.PACKAGE_NAME:
+            case AtomicFormula.APP_CERTIFICATE:
+            case AtomicFormula.INSTALLER_NAME:
+            case AtomicFormula.INSTALLER_CERTIFICATE:
+                return new AtomicFormula.StringAtomicFormula(key, value.toString(), isHashedValue);
+            case AtomicFormula.VERSION_CODE:
+                return new AtomicFormula.IntAtomicFormula(
+                        key, operator, Integer.parseInt(value.toString()));
+            case AtomicFormula.PRE_INSTALLED:
+                return new AtomicFormula.BooleanAtomicFormula(key, value.toString().equals("1"));
+            default:
+                throw new IllegalArgumentException(String.format("Unknown key: %d", key));
+        }
     }
 }
diff --git a/services/core/java/com/android/server/integrity/parser/RuleXmlParser.java b/services/core/java/com/android/server/integrity/parser/RuleXmlParser.java
index 2e99d0f..d405583 100644
--- a/services/core/java/com/android/server/integrity/parser/RuleXmlParser.java
+++ b/services/core/java/com/android/server/integrity/parser/RuleXmlParser.java
@@ -41,7 +41,7 @@
     private static final String NAMESPACE = "";
     private static final String RULE_LIST_TAG = "RL";
     private static final String RULE_TAG = "R";
-    private static final String OPEN_FORMULA_TAG = "OF";
+    private static final String COMPOUND_FORMULA_TAG = "OF";
     private static final String ATOMIC_FORMULA_TAG = "AF";
     private static final String EFFECT_ATTRIBUTE = "E";
     private static final String KEY_ATTRIBUTE = "K";
@@ -118,8 +118,8 @@
 
             if (eventType == XmlPullParser.START_TAG) {
                 switch (nodeName) {
-                    case OPEN_FORMULA_TAG:
-                        formula = parseOpenFormula(parser);
+                    case COMPOUND_FORMULA_TAG:
+                        formula = parseCompoundFormula(parser);
                         break;
                     case ATOMIC_FORMULA_TAG:
                         formula = parseAtomicFormula(parser);
@@ -137,7 +137,7 @@
         return new Rule(formula, effect);
     }
 
-    private static Formula parseOpenFormula(XmlPullParser parser)
+    private static Formula parseCompoundFormula(XmlPullParser parser)
             throws IOException, XmlPullParserException {
         int connector =
                 Integer.parseInt(extractAttributeValue(parser, CONNECTOR_ATTRIBUTE).orElse("-1"));
@@ -147,7 +147,8 @@
         while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
             String nodeName = parser.getName();
 
-            if (eventType == XmlPullParser.END_TAG && parser.getName().equals(OPEN_FORMULA_TAG)) {
+            if (eventType == XmlPullParser.END_TAG
+                    && parser.getName().equals(COMPOUND_FORMULA_TAG)) {
                 break;
             }
 
@@ -156,8 +157,8 @@
                     case ATOMIC_FORMULA_TAG:
                         formulas.add(parseAtomicFormula(parser));
                         break;
-                    case OPEN_FORMULA_TAG:
-                        formulas.add(parseOpenFormula(parser));
+                    case COMPOUND_FORMULA_TAG:
+                        formulas.add(parseCompoundFormula(parser));
                         break;
                     default:
                         throw new RuntimeException(
diff --git a/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java b/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java
index d4f41eb..b988fd4 100644
--- a/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java
+++ b/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java
@@ -16,24 +16,155 @@
 
 package com.android.server.integrity.serializer;
 
+import static com.android.server.integrity.model.ComponentBitSize.ATOMIC_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_END;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.CONNECTOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.DEFAULT_FORMAT_VERSION;
+import static com.android.server.integrity.model.ComponentBitSize.EFFECT_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.FORMAT_VERSION_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.KEY_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.OPERATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.SEPARATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.VALUE_SIZE_BITS;
+
+import android.content.integrity.AtomicFormula;
+import android.content.integrity.CompoundFormula;
+import android.content.integrity.Formula;
 import android.content.integrity.Rule;
 
+import com.android.server.integrity.model.BitOutputStream;
+
+import java.io.ByteArrayOutputStream;
 import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Optional;
 
-/** A helper class to serialize rules from the {@link Rule} model to Xml representation. */
+/** A helper class to serialize rules from the {@link Rule} model to Binary representation. */
 public class RuleBinarySerializer implements RuleSerializer {
 
+    // Get the byte representation for a list of rules, and write them to an output stream.
     @Override
     public void serialize(
-            List<Rule> rules, Optional<Integer> formatVersion, OutputStream outputStream) {
-        // TODO: Implement stream serializer.
+            List<Rule> rules, Optional<Integer> formatVersion, OutputStream outputStream)
+            throws RuleSerializeException {
+        try {
+            BitOutputStream bitOutputStream = new BitOutputStream();
+
+            int formatVersionValue = formatVersion.orElse(DEFAULT_FORMAT_VERSION);
+            bitOutputStream.setNext(FORMAT_VERSION_BITS, formatVersionValue);
+            outputStream.write(bitOutputStream.toByteArray());
+
+            for (Rule rule : rules) {
+                bitOutputStream.clear();
+                serializeRule(rule, bitOutputStream);
+                outputStream.write(bitOutputStream.toByteArray());
+            }
+        } catch (Exception e) {
+            throw new RuleSerializeException(e.getMessage(), e);
+        }
     }
 
+    // Get the byte representation for a list of rules.
     @Override
-    public byte[] serialize(List<Rule> rules, Optional<Integer> formatVersion) {
-        // TODO: Implement text serializer.
-        return null;
+    public byte[] serialize(List<Rule> rules, Optional<Integer> formatVersion)
+            throws RuleSerializeException {
+        try {
+            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            serialize(rules, formatVersion, byteArrayOutputStream);
+            return byteArrayOutputStream.toByteArray();
+        } catch (Exception e) {
+            throw new RuleSerializeException(e.getMessage(), e);
+        }
+    }
+
+    private void serializeRule(Rule rule, BitOutputStream bitOutputStream) {
+        if (rule == null) {
+            throw new IllegalArgumentException("Null rule can not be serialized");
+        }
+
+        // Start with a '1' bit to mark the start of a rule.
+        bitOutputStream.setNext();
+
+        serializeFormula(rule.getFormula(), bitOutputStream);
+        bitOutputStream.setNext(EFFECT_BITS, rule.getEffect());
+
+        // End with a '1' bit to mark the end of a rule.
+        bitOutputStream.setNext();
+    }
+
+    private void serializeFormula(Formula formula, BitOutputStream bitOutputStream) {
+        if (formula instanceof AtomicFormula) {
+            serializeAtomicFormula((AtomicFormula) formula, bitOutputStream);
+        } else if (formula instanceof CompoundFormula) {
+            serializeCompoundFormula((CompoundFormula) formula, bitOutputStream);
+        } else {
+            throw new IllegalArgumentException(
+                    String.format("Invalid formula type: %s", formula.getClass()));
+        }
+    }
+
+    private void serializeCompoundFormula(
+            CompoundFormula compoundFormula, BitOutputStream bitOutputStream) {
+        if (compoundFormula == null) {
+            throw new IllegalArgumentException("Null compound formula can not be serialized");
+        }
+
+        bitOutputStream.setNext(SEPARATOR_BITS, COMPOUND_FORMULA_START);
+        bitOutputStream.setNext(CONNECTOR_BITS, compoundFormula.getConnector());
+        for (Formula formula : compoundFormula.getFormulas()) {
+            serializeFormula(formula, bitOutputStream);
+        }
+        bitOutputStream.setNext(SEPARATOR_BITS, COMPOUND_FORMULA_END);
+    }
+
+    private void serializeAtomicFormula(
+            AtomicFormula atomicFormula, BitOutputStream bitOutputStream) {
+        if (atomicFormula == null) {
+            throw new IllegalArgumentException("Null atomic formula can not be serialized");
+        }
+
+        bitOutputStream.setNext(SEPARATOR_BITS, ATOMIC_FORMULA_START);
+        bitOutputStream.setNext(KEY_BITS, atomicFormula.getKey());
+        if (atomicFormula instanceof AtomicFormula.StringAtomicFormula) {
+            AtomicFormula.StringAtomicFormula stringAtomicFormula =
+                    (AtomicFormula.StringAtomicFormula) atomicFormula;
+            bitOutputStream.setNext(OPERATOR_BITS, AtomicFormula.EQ);
+            serializeValue(
+                    stringAtomicFormula.getValue(),
+                    stringAtomicFormula.getIsHashedValue(),
+                    bitOutputStream);
+        } else if (atomicFormula instanceof AtomicFormula.IntAtomicFormula) {
+            AtomicFormula.IntAtomicFormula intAtomicFormula =
+                    (AtomicFormula.IntAtomicFormula) atomicFormula;
+            bitOutputStream.setNext(OPERATOR_BITS, intAtomicFormula.getOperator());
+            serializeValue(
+                    String.valueOf(intAtomicFormula.getValue()),
+                    /* isHashedValue= */ false,
+                    bitOutputStream);
+        } else if (atomicFormula instanceof AtomicFormula.BooleanAtomicFormula) {
+            AtomicFormula.BooleanAtomicFormula booleanAtomicFormula =
+                    (AtomicFormula.BooleanAtomicFormula) atomicFormula;
+            bitOutputStream.setNext(OPERATOR_BITS, AtomicFormula.EQ);
+            serializeValue(
+                    booleanAtomicFormula.getValue() ? "1" : "0",
+                    /* isHashedValue= */ false,
+                    bitOutputStream);
+        } else {
+            throw new IllegalArgumentException(
+                    String.format("Invalid atomic formula type: %s", atomicFormula.getClass()));
+        }
+    }
+
+    private void serializeValue(
+            String value, boolean isHashedValue, BitOutputStream bitOutputStream) {
+        byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
+
+        bitOutputStream.setNext(isHashedValue);
+        bitOutputStream.setNext(VALUE_SIZE_BITS, valueBytes.length);
+        for (byte valueByte : valueBytes) {
+            bitOutputStream.setNext(/* numOfBits= */ 8, valueByte);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java b/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java
index 3ec9cf2..72068ce 100644
--- a/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java
+++ b/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java
@@ -39,7 +39,7 @@
 
     private static final String RULE_LIST_TAG = "RL";
     private static final String RULE_TAG = "R";
-    private static final String OPEN_FORMULA_TAG = "OF";
+    private static final String COMPOUND_FORMULA_TAG = "OF";
     private static final String ATOMIC_FORMULA_TAG = "AF";
     private static final String EFFECT_ATTRIBUTE = "E";
     private static final String KEY_ATTRIBUTE = "K";
@@ -78,13 +78,13 @@
     private void serializeRules(List<Rule> rules, XmlSerializer xmlSerializer) throws IOException {
         xmlSerializer.startTag(NAMESPACE, RULE_LIST_TAG);
         for (Rule rule : rules) {
-            serialize(rule, xmlSerializer);
+            serializeRule(rule, xmlSerializer);
         }
         xmlSerializer.endTag(NAMESPACE, RULE_LIST_TAG);
         xmlSerializer.endDocument();
     }
 
-    private void serialize(Rule rule, XmlSerializer xmlSerializer) throws IOException {
+    private void serializeRule(Rule rule, XmlSerializer xmlSerializer) throws IOException {
         if (rule == null) {
             return;
         }
@@ -98,25 +98,25 @@
         if (formula instanceof AtomicFormula) {
             serializeAtomicFormula((AtomicFormula) formula, xmlSerializer);
         } else if (formula instanceof CompoundFormula) {
-            serializeOpenFormula((CompoundFormula) formula, xmlSerializer);
+            serializeCompoundFormula((CompoundFormula) formula, xmlSerializer);
         } else {
             throw new IllegalArgumentException(
                     String.format("Invalid formula type: %s", formula.getClass()));
         }
     }
 
-    private void serializeOpenFormula(CompoundFormula compoundFormula, XmlSerializer xmlSerializer)
-            throws IOException {
+    private void serializeCompoundFormula(
+            CompoundFormula compoundFormula, XmlSerializer xmlSerializer) throws IOException {
         if (compoundFormula == null) {
             return;
         }
-        xmlSerializer.startTag(NAMESPACE, OPEN_FORMULA_TAG);
+        xmlSerializer.startTag(NAMESPACE, COMPOUND_FORMULA_TAG);
         serializeAttributeValue(
                 CONNECTOR_ATTRIBUTE, String.valueOf(compoundFormula.getConnector()), xmlSerializer);
         for (Formula formula : compoundFormula.getFormulas()) {
             serializeFormula(formula, xmlSerializer);
         }
-        xmlSerializer.endTag(NAMESPACE, OPEN_FORMULA_TAG);
+        xmlSerializer.endTag(NAMESPACE, COMPOUND_FORMULA_TAG);
     }
 
     private void serializeAtomicFormula(AtomicFormula atomicFormula, XmlSerializer xmlSerializer)
diff --git a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
index 0b970bf..340fe3d 100644
--- a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
+++ b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
@@ -150,7 +150,8 @@
             if (!mCallback.isAutoTimeDetectionEnabled()) {
                 if (DBG) {
                     Slog.d(LOG_TAG, "Auto time detection is not enabled."
-                            + " time=" + time
+                            + " origin=" + origin
+                            + ", time=" + time
                             + ", cause=" + cause);
                 }
                 return;
@@ -159,7 +160,8 @@
             if (mCallback.isAutoTimeDetectionEnabled()) {
                 if (DBG) {
                     Slog.d(LOG_TAG, "Auto time detection is enabled."
-                            + " time=" + time
+                            + " origin=" + origin
+                            + ", time=" + time
                             + ", cause=" + cause);
                 }
                 return;
@@ -232,16 +234,16 @@
 
     @Override
     public synchronized void dump(@NonNull PrintWriter pw, @Nullable String[] args) {
-        pw.println("mLastPhoneSuggestion=" + mLastPhoneSuggestion);
-        pw.println("mLastAutoSystemClockTimeSet=" + mLastAutoSystemClockTimeSet);
-        pw.println("mLastAutoSystemClockTime=" + mLastAutoSystemClockTime);
-        pw.println("mLastAutoSystemClockTimeSendNetworkBroadcast="
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+        ipw.println("TimeDetectorStrategy:");
+        ipw.increaseIndent(); // level 1
+
+        ipw.println("mLastPhoneSuggestion=" + mLastPhoneSuggestion);
+        ipw.println("mLastAutoSystemClockTimeSet=" + mLastAutoSystemClockTimeSet);
+        ipw.println("mLastAutoSystemClockTime=" + mLastAutoSystemClockTime);
+        ipw.println("mLastAutoSystemClockTimeSendNetworkBroadcast="
                 + mLastAutoSystemClockTimeSendNetworkBroadcast);
 
-        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
-
-        ipw.println("TimeDetectorStrategyImpl logs:");
-        ipw.increaseIndent(); // level 1
 
         ipw.println("Time change log:");
         ipw.increaseIndent(); // level 2
@@ -249,6 +251,7 @@
         ipw.decreaseIndent(); // level 2
 
         ipw.decreaseIndent(); // level 1
+        ipw.flush();
     }
 
     @GuardedBy("this")
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
index 32cee2d..0a6c2e7 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
@@ -39,6 +39,12 @@
 
     /**
      * The interface used by the strategy to interact with the surrounding service.
+     *
+     * <p>Note: Because the system properties-derived value {@link #isAutoTimeDetectionEnabled()}
+     * can be modified independently and from different threads (and processes!). its use is prone
+     * to race conditions. That will be true until the responsibility for setting their values is
+     * moved to {@link TimeDetectorStrategy}. There are similar issues with
+     * {@link #systemClockMillis()} while any process can modify the system clock.
      */
     interface Callback {
 
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index fb42507..3a07a69 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -40,6 +40,7 @@
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
     <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+    <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
     <uses-permission android:name="android.permission.PACKET_KEEPALIVE_OFFLOAD" />
diff --git a/services/tests/servicestests/src/com/android/server/integrity/parser/RuleBinaryParserTest.java b/services/tests/servicestests/src/com/android/server/integrity/parser/RuleBinaryParserTest.java
new file mode 100644
index 0000000..88b6d70
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/integrity/parser/RuleBinaryParserTest.java
@@ -0,0 +1,628 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.integrity.parser;
+
+import static com.android.server.integrity.model.ComponentBitSize.ATOMIC_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_END;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.CONNECTOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.DEFAULT_FORMAT_VERSION;
+import static com.android.server.integrity.model.ComponentBitSize.EFFECT_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.FORMAT_VERSION_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.KEY_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.OPERATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.SEPARATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.VALUE_SIZE_BITS;
+import static com.android.server.integrity.utils.TestUtils.getBits;
+import static com.android.server.integrity.utils.TestUtils.getBytes;
+import static com.android.server.integrity.utils.TestUtils.getValueBits;
+import static com.android.server.testutils.TestUtils.assertExpectException;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.integrity.AtomicFormula;
+import android.content.integrity.CompoundFormula;
+import android.content.integrity.Rule;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class RuleBinaryParserTest {
+
+    private static final String COMPOUND_FORMULA_START_BITS =
+            getBits(COMPOUND_FORMULA_START, SEPARATOR_BITS);
+    private static final String COMPOUND_FORMULA_END_BITS =
+            getBits(COMPOUND_FORMULA_END, SEPARATOR_BITS);
+    private static final String ATOMIC_FORMULA_START_BITS =
+            getBits(ATOMIC_FORMULA_START, SEPARATOR_BITS);
+    private static final int INVALID_FORMULA_SEPARATOR_VALUE = 3;
+    private static final String INVALID_FORMULA_SEPARATOR_BITS =
+            getBits(INVALID_FORMULA_SEPARATOR_VALUE, SEPARATOR_BITS);
+
+    private static final String NOT = getBits(CompoundFormula.NOT, CONNECTOR_BITS);
+    private static final String AND = getBits(CompoundFormula.AND, CONNECTOR_BITS);
+    private static final String OR = getBits(CompoundFormula.OR, CONNECTOR_BITS);
+    private static final int INVALID_CONNECTOR_VALUE = 3;
+    private static final String INVALID_CONNECTOR =
+            getBits(INVALID_CONNECTOR_VALUE, CONNECTOR_BITS);
+
+    private static final String PACKAGE_NAME = getBits(AtomicFormula.PACKAGE_NAME, KEY_BITS);
+    private static final String APP_CERTIFICATE = getBits(AtomicFormula.APP_CERTIFICATE, KEY_BITS);
+    private static final String VERSION_CODE = getBits(AtomicFormula.VERSION_CODE, KEY_BITS);
+    private static final String PRE_INSTALLED = getBits(AtomicFormula.PRE_INSTALLED, KEY_BITS);
+    private static final int INVALID_KEY_VALUE = 6;
+    private static final String INVALID_KEY = getBits(INVALID_KEY_VALUE, KEY_BITS);
+
+    private static final String EQ = getBits(AtomicFormula.EQ, OPERATOR_BITS);
+    private static final int INVALID_OPERATOR_VALUE = 5;
+    private static final String INVALID_OPERATOR = getBits(INVALID_OPERATOR_VALUE, OPERATOR_BITS);
+
+    private static final String IS_NOT_HASHED = "0";
+
+    private static final String DENY = getBits(Rule.DENY, EFFECT_BITS);
+    private static final int INVALID_EFFECT_VALUE = 5;
+    private static final String INVALID_EFFECT = getBits(INVALID_EFFECT_VALUE, EFFECT_BITS);
+
+    private static final String START_BIT = "1";
+    private static final String END_BIT = "1";
+    private static final String INVALID_MARKER_BIT = "0";
+
+    private static final byte[] DEFAULT_FORMAT_VERSION_BYTES =
+            getBytes(getBits(DEFAULT_FORMAT_VERSION, FORMAT_VERSION_BITS));
+
+    @Test
+    public void testBinaryStream_validCompoundFormula() throws Exception {
+        String packageName = "com.test.app";
+        String ruleBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + NOT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+        InputStream inputStream = new ByteArrayInputStream(rule.array());
+        Rule expectedRule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.NOT,
+                                Collections.singletonList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+
+        List<Rule> rules = binaryParser.parse(inputStream);
+
+        assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+    }
+
+    @Test
+    public void testBinaryString_validCompoundFormula_notConnector() throws Exception {
+        String packageName = "com.test.app";
+        String ruleBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + NOT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+        Rule expectedRule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.NOT,
+                                Collections.singletonList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+
+        List<Rule> rules = binaryParser.parse(rule.array());
+
+        assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+    }
+
+    @Test
+    public void testBinaryString_validCompoundFormula_andConnector() throws Exception {
+        String packageName = "com.test.app";
+        String appCertificate = "test_cert";
+        String ruleBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + AND
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + ATOMIC_FORMULA_START_BITS
+                        + APP_CERTIFICATE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(appCertificate.length(), VALUE_SIZE_BITS)
+                        + getValueBits(appCertificate)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+        Rule expectedRule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.AND,
+                                Arrays.asList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false),
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.APP_CERTIFICATE,
+                                                appCertificate,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+        List<Rule> rules = binaryParser.parse(rule.array());
+
+        assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+    }
+
+    @Test
+    public void testBinaryString_validCompoundFormula_orConnector() throws Exception {
+        String packageName = "com.test.app";
+        String appCertificate = "test_cert";
+        String ruleBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + OR
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + ATOMIC_FORMULA_START_BITS
+                        + APP_CERTIFICATE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(appCertificate.length(), VALUE_SIZE_BITS)
+                        + getValueBits(appCertificate)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+        Rule expectedRule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.OR,
+                                Arrays.asList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false),
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.APP_CERTIFICATE,
+                                                appCertificate,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+
+        List<Rule> rules = binaryParser.parse(rule.array());
+
+        assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+    }
+
+    @Test
+    public void testBinaryString_validAtomicFormula_stringValue() throws Exception {
+        String packageName = "com.test.app";
+        String ruleBits =
+                START_BIT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+        Rule expectedRule =
+                new Rule(
+                        new AtomicFormula.StringAtomicFormula(
+                                AtomicFormula.PACKAGE_NAME,
+                                packageName,
+                                /* isHashedValue= */ false),
+                        Rule.DENY);
+
+        List<Rule> rules = binaryParser.parse(rule.array());
+
+        assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+    }
+
+    @Test
+    public void testBinaryString_validAtomicFormula_integerValue() throws Exception {
+        String versionCode = "1";
+        String ruleBits =
+                START_BIT
+                        + ATOMIC_FORMULA_START_BITS
+                        + VERSION_CODE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(versionCode.length(), VALUE_SIZE_BITS)
+                        + getValueBits(versionCode)
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+        Rule expectedRule =
+                new Rule(
+                        new AtomicFormula.IntAtomicFormula(
+                                AtomicFormula.VERSION_CODE, AtomicFormula.EQ, 1),
+                        Rule.DENY);
+
+        List<Rule> rules = binaryParser.parse(rule.array());
+
+        assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+    }
+
+    @Test
+    public void testBinaryString_validAtomicFormula_booleanValue() throws Exception {
+        String isPreInstalled = "1";
+        String ruleBits =
+                START_BIT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PRE_INSTALLED
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(isPreInstalled.length(), VALUE_SIZE_BITS)
+                        + getValueBits(isPreInstalled)
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+        Rule expectedRule =
+                new Rule(
+                        new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true),
+                        Rule.DENY);
+
+        List<Rule> rules = binaryParser.parse(rule.array());
+
+        assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+    }
+
+    @Test
+    public void testBinaryString_invalidAtomicFormula() throws Exception {
+        String versionCode = "test";
+        String ruleBits =
+                START_BIT
+                        + ATOMIC_FORMULA_START_BITS
+                        + VERSION_CODE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(versionCode.length(), VALUE_SIZE_BITS)
+                        + getValueBits(versionCode)
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+
+        assertExpectException(
+                RuleParseException.class,
+                /* expectedExceptionMessageRegex */ "For input string:",
+                () -> binaryParser.parse(rule.array()));
+    }
+
+    @Test
+    public void testBinaryString_withNoRuleList() throws RuleParseException {
+        ByteBuffer rule = ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        RuleParser binaryParser = new RuleBinaryParser();
+
+        List<Rule> rules = binaryParser.parse(rule.array());
+
+        assertThat(rules).isEmpty();
+    }
+
+    @Test
+    public void testBinaryString_withEmptyRule() throws RuleParseException {
+        String ruleBits = START_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+
+        assertExpectException(
+                RuleParseException.class,
+                /* expectedExceptionMessageRegex */ "Invalid byte index",
+                () -> binaryParser.parse(rule.array()));
+    }
+
+    @Test
+    public void testBinaryString_invalidCompoundFormula_invalidNumberOfFormulas() throws Exception {
+        String packageName = "com.test.app";
+        String appCertificate = "test_cert";
+        String ruleBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + NOT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + ATOMIC_FORMULA_START_BITS
+                        + APP_CERTIFICATE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(appCertificate.length(), VALUE_SIZE_BITS)
+                        + getValueBits(appCertificate)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+
+        assertExpectException(
+                RuleParseException.class,
+                /* expectedExceptionMessageRegex */ "Connector NOT must have 1 formula only",
+                () -> binaryParser.parse(rule.array()));
+    }
+
+    @Test
+    public void testBinaryString_invalidRule_invalidOperator() throws Exception {
+        String versionCode = "1";
+        String ruleBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + NOT
+                        + ATOMIC_FORMULA_START_BITS
+                        + VERSION_CODE
+                        + INVALID_OPERATOR
+                        + IS_NOT_HASHED
+                        + getBits(versionCode.length(), VALUE_SIZE_BITS)
+                        + getValueBits(versionCode)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+
+        assertExpectException(
+                RuleParseException.class,
+                /* expectedExceptionMessageRegex */ String.format(
+                        "Unknown operator: %d", INVALID_OPERATOR_VALUE),
+                () -> binaryParser.parse(rule.array()));
+    }
+
+    @Test
+    public void testBinaryString_invalidRule_invalidEffect() throws Exception {
+        String packageName = "com.test.app";
+        String ruleBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + NOT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END_BITS
+                        + INVALID_EFFECT
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+
+        assertExpectException(
+                RuleParseException.class,
+                /* expectedExceptionMessageRegex */ String.format(
+                        "Unknown effect: %d", INVALID_EFFECT_VALUE),
+                () -> binaryParser.parse(rule.array()));
+    }
+
+    @Test
+    public void testBinaryString_invalidRule_invalidConnector() throws Exception {
+        String packageName = "com.test.app";
+        String ruleBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + INVALID_CONNECTOR
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+
+        assertExpectException(
+                RuleParseException.class,
+                /* expectedExceptionMessageRegex */ String.format(
+                        "Unknown connector: %d", INVALID_CONNECTOR_VALUE),
+                () -> binaryParser.parse(rule.array()));
+    }
+
+    @Test
+    public void testBinaryString_invalidRule_invalidKey() throws Exception {
+        String packageName = "com.test.app";
+        String ruleBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + NOT
+                        + ATOMIC_FORMULA_START_BITS
+                        + INVALID_KEY
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+
+        assertExpectException(
+                RuleParseException.class,
+                /* expectedExceptionMessageRegex */ String.format(
+                        "Unknown key: %d", INVALID_KEY_VALUE),
+                () -> binaryParser.parse(rule.array()));
+    }
+
+    @Test
+    public void testBinaryString_invalidRule_invalidSeparator() throws Exception {
+        String packageName = "com.test.app";
+        String ruleBits =
+                START_BIT
+                        + INVALID_FORMULA_SEPARATOR_BITS
+                        + NOT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+
+        assertExpectException(
+                RuleParseException.class,
+                /* expectedExceptionMessageRegex */ String.format(
+                        "Unknown formula separator: %d", INVALID_FORMULA_SEPARATOR_VALUE),
+                () -> binaryParser.parse(rule.array()));
+    }
+
+    @Test
+    public void testBinaryString_invalidRule_invalidEndMarker() throws Exception {
+        String packageName = "com.test.app";
+        String ruleBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + NOT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + INVALID_MARKER_BIT;
+        byte[] ruleBytes = getBytes(ruleBits);
+        ByteBuffer rule =
+                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+        rule.put(ruleBytes);
+        RuleParser binaryParser = new RuleBinaryParser();
+
+        assertExpectException(
+                RuleParseException.class,
+                /* expectedExceptionMessageRegex */ "A rule must end with a '1' bit",
+                () -> binaryParser.parse(rule.array()));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java
new file mode 100644
index 0000000..901277d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.integrity.serializer;
+
+import static com.android.server.integrity.model.ComponentBitSize.ATOMIC_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_END;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.CONNECTOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.DEFAULT_FORMAT_VERSION;
+import static com.android.server.integrity.model.ComponentBitSize.EFFECT_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.FORMAT_VERSION_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.KEY_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.OPERATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.SEPARATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.VALUE_SIZE_BITS;
+import static com.android.server.integrity.utils.TestUtils.getBits;
+import static com.android.server.integrity.utils.TestUtils.getBytes;
+import static com.android.server.integrity.utils.TestUtils.getValueBits;
+import static com.android.server.testutils.TestUtils.assertExpectException;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.integrity.AppInstallMetadata;
+import android.content.integrity.AtomicFormula;
+import android.content.integrity.CompoundFormula;
+import android.content.integrity.Formula;
+import android.content.integrity.Rule;
+
+import androidx.annotation.NonNull;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Optional;
+
+@RunWith(JUnit4.class)
+public class RuleBinarySerializerTest {
+
+    private static final String COMPOUND_FORMULA_START_BITS =
+            getBits(COMPOUND_FORMULA_START, SEPARATOR_BITS);
+    private static final String COMPOUND_FORMULA_END_BITS =
+            getBits(COMPOUND_FORMULA_END, SEPARATOR_BITS);
+    private static final String ATOMIC_FORMULA_START_BITS =
+            getBits(ATOMIC_FORMULA_START, SEPARATOR_BITS);
+
+    private static final String NOT = getBits(CompoundFormula.NOT, CONNECTOR_BITS);
+    private static final String AND = getBits(CompoundFormula.AND, CONNECTOR_BITS);
+    private static final String OR = getBits(CompoundFormula.OR, CONNECTOR_BITS);
+
+    private static final String PACKAGE_NAME = getBits(AtomicFormula.PACKAGE_NAME, KEY_BITS);
+    private static final String APP_CERTIFICATE = getBits(AtomicFormula.APP_CERTIFICATE, KEY_BITS);
+    private static final String VERSION_CODE = getBits(AtomicFormula.VERSION_CODE, KEY_BITS);
+    private static final String PRE_INSTALLED = getBits(AtomicFormula.PRE_INSTALLED, KEY_BITS);
+
+    private static final String EQ = getBits(AtomicFormula.EQ, OPERATOR_BITS);
+
+    private static final String IS_NOT_HASHED = "0";
+
+    private static final String DENY = getBits(Rule.DENY, EFFECT_BITS);
+
+    private static final String START_BIT = "1";
+    private static final String END_BIT = "1";
+
+    private static final byte[] DEFAULT_FORMAT_VERSION_BYTES =
+            getBytes(getBits(DEFAULT_FORMAT_VERSION, FORMAT_VERSION_BITS));
+
+    @Test
+    public void testBinaryString_serializeEmptyRule() throws Exception {
+        Rule rule = null;
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+
+        assertExpectException(
+                RuleSerializeException.class,
+                /* expectedExceptionMessageRegex= */ "Null rule can not be serialized",
+                () ->
+                        binarySerializer.serialize(
+                                Collections.singletonList(rule),
+                                /* formatVersion= */ Optional.empty()));
+    }
+
+    @Test
+    public void testBinaryStream_serializeValidCompoundFormula() throws Exception {
+        String packageName = "com.test.app";
+        Rule rule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.NOT,
+                                Collections.singletonList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        String expectedBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + NOT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        binarySerializer.serialize(
+                Collections.singletonList(rule),
+                /* formatVersion= */ Optional.empty(),
+                outputStream);
+
+        byte[] actualRules = outputStream.toByteArray();
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidCompoundFormula_notConnector() throws Exception {
+        String packageName = "com.test.app";
+        Rule rule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.NOT,
+                                Collections.singletonList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + NOT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidCompoundFormula_andConnector() throws Exception {
+        String packageName = "com.test.app";
+        String appCertificate = "test_cert";
+        Rule rule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.AND,
+                                Arrays.asList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false),
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.APP_CERTIFICATE,
+                                                appCertificate,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + AND
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + ATOMIC_FORMULA_START_BITS
+                        + APP_CERTIFICATE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(appCertificate.length(), VALUE_SIZE_BITS)
+                        + getValueBits(appCertificate)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidCompoundFormula_orConnector() throws Exception {
+        String packageName = "com.test.app";
+        String appCertificate = "test_cert";
+        Rule rule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.OR,
+                                Arrays.asList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false),
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.APP_CERTIFICATE,
+                                                appCertificate,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START_BITS
+                        + OR
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + ATOMIC_FORMULA_START_BITS
+                        + APP_CERTIFICATE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(appCertificate.length(), VALUE_SIZE_BITS)
+                        + getValueBits(appCertificate)
+                        + COMPOUND_FORMULA_END_BITS
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidAtomicFormula_stringValue() throws Exception {
+        String packageName = "com.test.app";
+        Rule rule =
+                new Rule(
+                        new AtomicFormula.StringAtomicFormula(
+                                AtomicFormula.PACKAGE_NAME,
+                                packageName,
+                                /* isHashedValue= */ false),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidAtomicFormula_integerValue() throws Exception {
+        String versionCode = "1";
+        Rule rule =
+                new Rule(
+                        new AtomicFormula.IntAtomicFormula(
+                                AtomicFormula.VERSION_CODE,
+                                AtomicFormula.EQ,
+                                Integer.parseInt(versionCode)),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + ATOMIC_FORMULA_START_BITS
+                        + VERSION_CODE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(versionCode.length(), VALUE_SIZE_BITS)
+                        + getValueBits(versionCode)
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidAtomicFormula_booleanValue() throws Exception {
+        String preInstalled = "1";
+        Rule rule =
+                new Rule(
+                        new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + ATOMIC_FORMULA_START_BITS
+                        + PRE_INSTALLED
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(preInstalled.length(), VALUE_SIZE_BITS)
+                        + getValueBits(preInstalled)
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeInvalidFormulaType() throws Exception {
+        Formula invalidFormula = getInvalidFormula();
+        Rule rule = new Rule(invalidFormula, Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+
+        assertExpectException(
+                RuleSerializeException.class,
+                /* expectedExceptionMessageRegex= */ "Invalid formula type",
+                () ->
+                        binarySerializer.serialize(
+                                Collections.singletonList(rule),
+                                /* formatVersion= */ Optional.empty()));
+    }
+
+    @Test
+    public void testBinaryString_serializeFormatVersion() throws Exception {
+        int formatVersion = 1;
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits = getBits(formatVersion, FORMAT_VERSION_BITS);
+        byte[] expectedRules = getBytes(expectedBits);
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.emptyList(), /* formatVersion= */ Optional.of(formatVersion));
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    private static Formula getInvalidFormula() {
+        return new Formula() {
+            @Override
+            public boolean isSatisfied(AppInstallMetadata appInstallMetadata) {
+                return false;
+            }
+
+            @Override
+            public int getTag() {
+                return 0;
+            }
+
+            @Override
+            public int hashCode() {
+                return super.hashCode();
+            }
+
+            @Override
+            public boolean equals(Object obj) {
+                return super.equals(obj);
+            }
+
+            @NonNull
+            @Override
+            protected Object clone() throws CloneNotSupportedException {
+                return super.clone();
+            }
+
+            @Override
+            public String toString() {
+                return super.toString();
+            }
+
+            @Override
+            protected void finalize() throws Throwable {
+                super.finalize();
+            }
+        };
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java
index 5903b5a..ad74901 100644
--- a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java
+++ b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java
@@ -91,7 +91,7 @@
     }
 
     @Test
-    public void testXmlStream_serializeValidOpenFormula() throws Exception {
+    public void testXmlStream_serializeValidCompoundFormula() throws Exception {
         Rule rule =
                 new Rule(
                         new CompoundFormula(
@@ -134,7 +134,7 @@
     }
 
     @Test
-    public void testXmlString_serializeValidOpenFormula_notConnector() throws Exception {
+    public void testXmlString_serializeValidCompoundFormula_notConnector() throws Exception {
         Rule rule =
                 new Rule(
                         new CompoundFormula(
@@ -174,7 +174,7 @@
     }
 
     @Test
-    public void testXmlString_serializeValidOpenFormula_andConnector() throws Exception {
+    public void testXmlString_serializeValidCompoundFormula_andConnector() throws Exception {
         Rule rule =
                 new Rule(
                         new CompoundFormula(
@@ -224,7 +224,7 @@
     }
 
     @Test
-    public void testXmlString_serializeValidOpenFormula_orConnector() throws Exception {
+    public void testXmlString_serializeValidCompoundFormula_orConnector() throws Exception {
         Rule rule =
                 new Rule(
                         new CompoundFormula(
diff --git a/services/tests/servicestests/src/com/android/server/integrity/utils/TestUtils.java b/services/tests/servicestests/src/com/android/server/integrity/utils/TestUtils.java
new file mode 100644
index 0000000..e54410b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/integrity/utils/TestUtils.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.integrity.utils;
+
+public class TestUtils {
+
+    public static String getBits(int component, int numOfBits) {
+        return String.format("%" + numOfBits + "s", Integer.toBinaryString(component))
+                .replace(' ', '0');
+    }
+
+    public static String getValueBits(String value) {
+        StringBuilder stringBuilder = new StringBuilder();
+        for (byte valueByte : value.getBytes()) {
+            stringBuilder.append(getBits(valueByte, /* numOfBits= */ 8));
+        }
+        return stringBuilder.toString();
+    }
+
+    public static byte[] getBytes(String bits) {
+        int bitStringSize = bits.length();
+        int numOfBytes = bitStringSize / 8;
+        if (bitStringSize % 8 != 0) {
+            numOfBytes++;
+        }
+        byte[] bytes = new byte[numOfBytes];
+        for (int i = 0; i < bits.length(); i++) {
+            if (bits.charAt(i) == '1') {
+                bytes[i / 8] |= 1 << (7 - (i % 8));
+            }
+        }
+        return bytes;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java
index 8a7edf7..7a0a28d 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java
@@ -640,9 +640,9 @@
 
     private static PhoneTimeSuggestion createPhoneTimeSuggestion(int phoneId,
             TimestampedValue<Long> utcTime) {
-        PhoneTimeSuggestion timeSuggestion = new PhoneTimeSuggestion(phoneId);
-        timeSuggestion.setUtcTime(utcTime);
-        return timeSuggestion;
+        return new PhoneTimeSuggestion.Builder(phoneId)
+                .setUtcTime(utcTime)
+                .build();
     }
 
     private ManualTimeSuggestion createManualTimeSuggestion(long timeMillis) {
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
index 9951e85..84b495f 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
@@ -140,10 +140,10 @@
 
     private static PhoneTimeSuggestion createPhoneTimeSuggestion() {
         int phoneId = 1234;
-        PhoneTimeSuggestion suggestion = new PhoneTimeSuggestion(phoneId);
         TimestampedValue<Long> timeValue = new TimestampedValue<>(100L, 1_000_000L);
-        suggestion.setUtcTime(timeValue);
-        return suggestion;
+        return new PhoneTimeSuggestion.Builder(phoneId)
+                .setUtcTime(timeValue)
+                .build();
     }
 
     private static ManualTimeSuggestion createManualTimeSuggestion() {
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 3bc3e49..e3981f6 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -8161,17 +8161,25 @@
         return Collections.EMPTY_LIST;
     }
 
-    /** @hide */
-    public List<String> getPackagesWithCarrierPrivilegesForAllPhones() {
+    /**
+     * Get the names of packages with carrier privileges for all the active subscriptions.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+    @NonNull
+    public List<String> getCarrierPrivilegedPackagesForAllActiveSubscriptions() {
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
                 return telephony.getPackagesWithCarrierPrivilegesForAllPhones();
             }
         } catch (RemoteException ex) {
-            Rlog.e(TAG, "getPackagesWithCarrierPrivilegesForAllPhones RemoteException", ex);
+            Rlog.e(TAG, "getCarrierPrivilegedPackagesForAllActiveSubscriptions RemoteException",
+                    ex);
         } catch (NullPointerException ex) {
-            Rlog.e(TAG, "getPackagesWithCarrierPrivilegesForAllPhones NPE", ex);
+            Rlog.e(TAG, "getCarrierPrivilegedPackagesForAllActiveSubscriptions NPE", ex);
         }
         return Collections.EMPTY_LIST;
     }