Merge "SpatializerHelper: report head tracking disabled" into tm-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
index f4faec8..8b8a57d 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -73,6 +73,7 @@
 
     private final Object mLock;
     private final Handler mHandler;
+    private final Analyst mAnalyst;
     private final InternalResourceService mIrs;
     private final Scribe mScribe;
 
@@ -110,10 +111,11 @@
      */
     private static final int MSG_CHECK_INDIVIDUAL_AFFORDABILITY = 1;
 
-    Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe) {
+    Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe, @NonNull Analyst analyst) {
         mLock = irs.getLock();
         mIrs = irs;
         mScribe = scribe;
+        mAnalyst = analyst;
         mHandler = new AgentHandler(TareHandlerThread.get().getLooper());
         mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
         mBalanceThresholdAlarmQueue = new BalanceThresholdAlarmQueue(
@@ -443,7 +445,7 @@
     void recordTransactionLocked(final int userId, @NonNull final String pkgName,
             @NonNull Ledger ledger, @NonNull Ledger.Transaction transaction,
             final boolean notifyOnAffordabilityChange) {
-        if (transaction.delta == 0) {
+        if (!DEBUG && transaction.delta == 0) {
             // Skip recording transactions with a delta of 0 to save on space.
             return;
         }
@@ -471,6 +473,7 @@
         }
         ledger.recordTransaction(transaction);
         mScribe.adjustRemainingConsumableCakesLocked(-transaction.ctp);
+        mAnalyst.noteTransaction(transaction);
         if (transaction.delta != 0 && notifyOnAffordabilityChange) {
             final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
                     mActionAffordabilityNotes.get(userId, pkgName);
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java b/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java
new file mode 100644
index 0000000..bc6fe7e5
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.tare;
+
+import static com.android.server.tare.EconomicPolicy.TYPE_ACTION;
+import static com.android.server.tare.EconomicPolicy.TYPE_REGULATION;
+import static com.android.server.tare.EconomicPolicy.TYPE_REWARD;
+import static com.android.server.tare.EconomicPolicy.getEventType;
+import static com.android.server.tare.TareUtils.cakeToString;
+
+import android.annotation.NonNull;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Responsible for maintaining statistics and analysis of TARE's performance.
+ */
+public class Analyst {
+    private static final String TAG = "TARE-" + Analyst.class.getSimpleName();
+    private static final boolean DEBUG = InternalResourceService.DEBUG
+            || Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int NUM_PERIODS_TO_RETAIN = 8;
+
+    static final class Report {
+        /** How much the battery was discharged over the tracked period. */
+        public int cumulativeBatteryDischarge = 0;
+        public int currentBatteryLevel = 0;
+        /**
+         * Profit from performing actions. This excludes special circumstances where we charge the
+         * app
+         * less than the action's CTP.
+         */
+        public long cumulativeProfit = 0;
+        public int numProfitableActions = 0;
+        /**
+         * Losses from performing actions for special circumstances (eg. for a TOP app) where we
+         * charge
+         * the app less than the action's CTP.
+         */
+        public long cumulativeLoss = 0;
+        public int numUnprofitableActions = 0;
+        /**
+         * The total number of rewards given to apps over this period.
+         */
+        public long cumulativeRewards = 0;
+        public int numRewards = 0;
+        /**
+         * Regulations that increased an app's balance.
+         */
+        public long cumulativePositiveRegulations = 0;
+        public int numPositiveRegulations = 0;
+        /**
+         * Regulations that decreased an app's balance.
+         */
+        public long cumulativeNegativeRegulations = 0;
+        public int numNegativeRegulations = 0;
+
+        private void clear() {
+            cumulativeBatteryDischarge = 0;
+            currentBatteryLevel = 0;
+            cumulativeProfit = 0;
+            numProfitableActions = 0;
+            cumulativeLoss = 0;
+            numUnprofitableActions = 0;
+            cumulativeRewards = 0;
+            numRewards = 0;
+            cumulativePositiveRegulations = 0;
+            numPositiveRegulations = 0;
+            cumulativeNegativeRegulations = 0;
+            numNegativeRegulations = 0;
+        }
+    }
+
+    private int mPeriodIndex = 0;
+    /** How much the battery was discharged over the tracked period. */
+    private final Report[] mReports = new Report[NUM_PERIODS_TO_RETAIN];
+
+    /** Returns the list of most recent reports, with the oldest report first. */
+    @NonNull
+    List<Report> getReports() {
+        final List<Report> list = new ArrayList<>(NUM_PERIODS_TO_RETAIN);
+        for (int i = 1; i <= NUM_PERIODS_TO_RETAIN; ++i) {
+            final int idx = (mPeriodIndex + i) % NUM_PERIODS_TO_RETAIN;
+            final Report report = mReports[idx];
+            if (report != null) {
+                list.add(report);
+            }
+        }
+        return list;
+    }
+
+    /**
+     * Tracks the given reports instead of whatever is currently saved. Reports should be ordered
+     * oldest to most recent.
+     */
+    void loadReports(@NonNull List<Report> reports) {
+        final int numReports = reports.size();
+        mPeriodIndex = Math.max(0, numReports - 1);
+        for (int i = 0; i < NUM_PERIODS_TO_RETAIN; ++i) {
+            if (i < numReports) {
+                mReports[i] = reports.get(i);
+            } else {
+                mReports[i] = null;
+            }
+        }
+    }
+
+    void noteBatteryLevelChange(int newBatteryLevel) {
+        if (newBatteryLevel == 100 && mReports[mPeriodIndex] != null
+                && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel) {
+            mPeriodIndex = (mPeriodIndex + 1) % NUM_PERIODS_TO_RETAIN;
+            if (mReports[mPeriodIndex] != null) {
+                final Report report = mReports[mPeriodIndex];
+                report.clear();
+                report.currentBatteryLevel = newBatteryLevel;
+                return;
+            }
+        }
+
+        if (mReports[mPeriodIndex] == null) {
+            Report report = new Report();
+            mReports[mPeriodIndex] = report;
+            report.currentBatteryLevel = newBatteryLevel;
+            return;
+        }
+
+        final Report report = mReports[mPeriodIndex];
+        if (newBatteryLevel < report.currentBatteryLevel) {
+            report.cumulativeBatteryDischarge += (report.currentBatteryLevel - newBatteryLevel);
+        }
+        report.currentBatteryLevel = newBatteryLevel;
+    }
+
+    void noteTransaction(@NonNull Ledger.Transaction transaction) {
+        if (mReports[mPeriodIndex] == null) {
+            mReports[mPeriodIndex] = new Report();
+        }
+        final Report report = mReports[mPeriodIndex];
+        switch (getEventType(transaction.eventId)) {
+            case TYPE_ACTION:
+                // For now, assume all instances where price < CTP is a special instance.
+                // TODO: add an explicit signal for special circumstances
+                if (-transaction.delta > transaction.ctp) {
+                    report.cumulativeProfit += (-transaction.delta - transaction.ctp);
+                    report.numProfitableActions++;
+                } else if (-transaction.delta < transaction.ctp) {
+                    report.cumulativeLoss += (transaction.ctp + transaction.delta);
+                    report.numUnprofitableActions++;
+                }
+                break;
+            case TYPE_REGULATION:
+                if (transaction.delta > 0) {
+                    report.cumulativePositiveRegulations += transaction.delta;
+                    report.numPositiveRegulations++;
+                } else if (transaction.delta < 0) {
+                    report.cumulativeNegativeRegulations -= transaction.delta;
+                    report.numNegativeRegulations++;
+                }
+                break;
+            case TYPE_REWARD:
+                if (transaction.delta != 0) {
+                    report.cumulativeRewards += transaction.delta;
+                    report.numRewards++;
+                }
+                break;
+        }
+    }
+
+    void tearDown() {
+        for (int i = 0; i < mReports.length; ++i) {
+            mReports[i] = null;
+        }
+        mPeriodIndex = 0;
+    }
+
+    @NonNull
+    private String padStringWithSpaces(@NonNull String text, int targetLength) {
+        // Make sure to have at least one space on either side.
+        final int padding = Math.max(2, targetLength - text.length()) >>> 1;
+        return " ".repeat(padding) + text + " ".repeat(padding);
+    }
+
+    void dump(IndentingPrintWriter pw) {
+        pw.println("Reports:");
+        pw.increaseIndent();
+        pw.print("      Total Discharge");
+        final int statColsLength = 47;
+        pw.print(padStringWithSpaces("Profit (avg/action : avg/discharge)", statColsLength));
+        pw.print(padStringWithSpaces("Loss (avg/action : avg/discharge)", statColsLength));
+        pw.print(padStringWithSpaces("Rewards (avg/reward : avg/discharge)", statColsLength));
+        pw.print(padStringWithSpaces("+Regs (avg/reg : avg/discharge)", statColsLength));
+        pw.print(padStringWithSpaces("-Regs (avg/reg : avg/discharge)", statColsLength));
+        pw.println();
+        for (int r = 0; r < NUM_PERIODS_TO_RETAIN; ++r) {
+            final int idx = (mPeriodIndex - r + NUM_PERIODS_TO_RETAIN) % NUM_PERIODS_TO_RETAIN;
+            final Report report = mReports[idx];
+            if (report == null) {
+                continue;
+            }
+            pw.print("t-");
+            pw.print(r);
+            pw.print(":  ");
+            pw.print(padStringWithSpaces(Integer.toString(report.cumulativeBatteryDischarge), 15));
+            if (report.numProfitableActions > 0) {
+                final String perDischarge = report.cumulativeBatteryDischarge > 0
+                        ? cakeToString(report.cumulativeProfit / report.cumulativeBatteryDischarge)
+                        : "N/A";
+                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
+                                cakeToString(report.cumulativeProfit),
+                                cakeToString(report.cumulativeProfit / report.numProfitableActions),
+                                perDischarge),
+                        statColsLength));
+            } else {
+                pw.print(padStringWithSpaces("N/A", statColsLength));
+            }
+            if (report.numUnprofitableActions > 0) {
+                final String perDischarge = report.cumulativeBatteryDischarge > 0
+                        ? cakeToString(report.cumulativeLoss / report.cumulativeBatteryDischarge)
+                        : "N/A";
+                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
+                                cakeToString(report.cumulativeLoss),
+                                cakeToString(report.cumulativeLoss / report.numUnprofitableActions),
+                                perDischarge),
+                        statColsLength));
+            } else {
+                pw.print(padStringWithSpaces("N/A", statColsLength));
+            }
+            if (report.numRewards > 0) {
+                final String perDischarge = report.cumulativeBatteryDischarge > 0
+                        ? cakeToString(report.cumulativeRewards / report.cumulativeBatteryDischarge)
+                        : "N/A";
+                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
+                                cakeToString(report.cumulativeRewards),
+                                cakeToString(report.cumulativeRewards / report.numRewards),
+                                perDischarge),
+                        statColsLength));
+            } else {
+                pw.print(padStringWithSpaces("N/A", statColsLength));
+            }
+            if (report.numPositiveRegulations > 0) {
+                final String perDischarge = report.cumulativeBatteryDischarge > 0
+                        ? cakeToString(
+                        report.cumulativePositiveRegulations / report.cumulativeBatteryDischarge)
+                        : "N/A";
+                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
+                                cakeToString(report.cumulativePositiveRegulations),
+                                cakeToString(report.cumulativePositiveRegulations
+                                        / report.numPositiveRegulations),
+                                perDischarge),
+                        statColsLength));
+            } else {
+                pw.print(padStringWithSpaces("N/A", statColsLength));
+            }
+            if (report.numNegativeRegulations > 0) {
+                final String perDischarge = report.cumulativeBatteryDischarge > 0
+                        ? cakeToString(
+                        report.cumulativeNegativeRegulations / report.cumulativeBatteryDischarge)
+                        : "N/A";
+                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
+                                cakeToString(report.cumulativeNegativeRegulations),
+                                cakeToString(report.cumulativeNegativeRegulations
+                                        / report.numNegativeRegulations),
+                                perDischarge),
+                        statColsLength));
+            } else {
+                pw.print(padStringWithSpaces("N/A", statColsLength));
+            }
+            pw.println();
+        }
+        pw.decreaseIndent();
+    }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
index 2118eeb..9c7d702 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -121,6 +121,7 @@
     private IDeviceIdleController mDeviceIdleController;
 
     private final Agent mAgent;
+    private final Analyst mAnalyst;
     private final ConfigObserver mConfigObserver;
     private final EconomyManagerStub mEconomyManagerStub;
     private final Scribe mScribe;
@@ -254,9 +255,10 @@
         mPackageManager = context.getPackageManager();
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         mEconomyManagerStub = new EconomyManagerStub();
-        mScribe = new Scribe(this);
+        mAnalyst = new Analyst();
+        mScribe = new Scribe(this, mAnalyst);
         mCompleteEconomicPolicy = new CompleteEconomicPolicy(this);
-        mAgent = new Agent(this, mScribe);
+        mAgent = new Agent(this, mScribe, mAnalyst);
 
         mConfigObserver = new ConfigObserver(mHandler, context);
 
@@ -375,6 +377,7 @@
     void onBatteryLevelChanged() {
         synchronized (mLock) {
             final int newBatteryLevel = getCurrentBatteryLevel();
+            mAnalyst.noteBatteryLevelChange(newBatteryLevel);
             final boolean increased = newBatteryLevel > mCurrentBatteryLevel;
             if (increased) {
                 mAgent.distributeBasicIncomeLocked(newBatteryLevel);
@@ -741,6 +744,7 @@
         }
         synchronized (mLock) {
             mAgent.tearDownLocked();
+            mAnalyst.tearDown();
             mCompleteEconomicPolicy.tearDown();
             mExemptedApps.clear();
             mExemptListLoaded = false;
@@ -1145,6 +1149,9 @@
 
             pw.println();
             mAgent.dumpLocked(pw);
+
+            pw.println();
+            mAnalyst.dump(pw);
         }
     }
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
index 7442877..941cc39 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
@@ -72,6 +72,7 @@
     private static final String XML_TAG_TARE = "tare";
     private static final String XML_TAG_TRANSACTION = "transaction";
     private static final String XML_TAG_USER = "user";
+    private static final String XML_TAG_PERIOD_REPORT = "report";
 
     private static final String XML_ATTR_CTP = "ctp";
     private static final String XML_ATTR_DELTA = "delta";
@@ -86,6 +87,18 @@
     private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime";
     private static final String XML_ATTR_REMAINING_CONSUMABLE_CAKES = "remainingConsumableCakes";
     private static final String XML_ATTR_CONSUMPTION_LIMIT = "consumptionLimit";
+    private static final String XML_ATTR_PR_DISCHARGE = "discharge";
+    private static final String XML_ATTR_PR_BATTERY_LEVEL = "batteryLevel";
+    private static final String XML_ATTR_PR_PROFIT = "profit";
+    private static final String XML_ATTR_PR_NUM_PROFIT = "numProfits";
+    private static final String XML_ATTR_PR_LOSS = "loss";
+    private static final String XML_ATTR_PR_NUM_LOSS = "numLoss";
+    private static final String XML_ATTR_PR_REWARDS = "rewards";
+    private static final String XML_ATTR_PR_NUM_REWARDS = "numRewards";
+    private static final String XML_ATTR_PR_POS_REGULATIONS = "posRegulations";
+    private static final String XML_ATTR_PR_NUM_POS_REGULATIONS = "numPosRegulations";
+    private static final String XML_ATTR_PR_NEG_REGULATIONS = "negRegulations";
+    private static final String XML_ATTR_PR_NUM_NEG_REGULATIONS = "numNegRegulations";
 
     /** Version of the file schema. */
     private static final int STATE_FILE_VERSION = 0;
@@ -94,6 +107,7 @@
 
     private final AtomicFile mStateFile;
     private final InternalResourceService mIrs;
+    private final Analyst mAnalyst;
 
     @GuardedBy("mIrs.getLock()")
     private long mLastReclamationTime;
@@ -107,13 +121,14 @@
     private final Runnable mCleanRunnable = this::cleanupLedgers;
     private final Runnable mWriteRunnable = this::writeState;
 
-    Scribe(InternalResourceService irs) {
-        this(irs, Environment.getDataSystemDirectory());
+    Scribe(InternalResourceService irs, Analyst analyst) {
+        this(irs, analyst, Environment.getDataSystemDirectory());
     }
 
     @VisibleForTesting
-    Scribe(InternalResourceService irs, File dataDir) {
+    Scribe(InternalResourceService irs, Analyst analyst, File dataDir) {
         mIrs = irs;
+        mAnalyst = analyst;
 
         final File tareDir = new File(dataDir, "tare");
         //noinspection ResultOfMethodCallIgnored
@@ -210,6 +225,7 @@
             }
         }
 
+        final List<Analyst.Report> reports = new ArrayList<>();
         try (FileInputStream fis = mStateFile.openRead()) {
             TypedXmlPullParser parser = Xml.resolvePullParser(fis);
 
@@ -263,11 +279,15 @@
                                 readUserFromXmlLocked(
                                         parser, installedPackagesPerUser, endTimeCutoff));
                         break;
+                    case XML_TAG_PERIOD_REPORT:
+                        reports.add(readReportFromXml(parser));
+                        break;
                     default:
                         Slog.e(TAG, "Unexpected tag: " + tagName);
                         break;
                 }
             }
+            mAnalyst.loadReports(reports);
             scheduleCleanup(earliestEndTime);
         } catch (IOException | XmlPullParserException e) {
             Slog.wtf(TAG, "Error reading state from disk", e);
@@ -457,6 +477,37 @@
         return earliestEndTime;
     }
 
+
+    /**
+     * @param parser Xml parser at the beginning of a {@link #XML_TAG_PERIOD_REPORT} tag. The next
+     *               "parser.next()" call will take the parser into the body of the report tag.
+     * @return Newly instantiated Report holding all the information we just read out of the xml tag
+     */
+    @NonNull
+    private static Analyst.Report readReportFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        final Analyst.Report report = new Analyst.Report();
+
+        report.cumulativeBatteryDischarge = parser.getAttributeInt(null, XML_ATTR_PR_DISCHARGE);
+        report.currentBatteryLevel = parser.getAttributeInt(null, XML_ATTR_PR_BATTERY_LEVEL);
+        report.cumulativeProfit = parser.getAttributeLong(null, XML_ATTR_PR_PROFIT);
+        report.numProfitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_PROFIT);
+        report.cumulativeLoss = parser.getAttributeLong(null, XML_ATTR_PR_LOSS);
+        report.numUnprofitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_LOSS);
+        report.cumulativeRewards = parser.getAttributeLong(null, XML_ATTR_PR_REWARDS);
+        report.numRewards = parser.getAttributeInt(null, XML_ATTR_PR_NUM_REWARDS);
+        report.cumulativePositiveRegulations =
+                parser.getAttributeLong(null, XML_ATTR_PR_POS_REGULATIONS);
+        report.numPositiveRegulations =
+                parser.getAttributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS);
+        report.cumulativeNegativeRegulations =
+                parser.getAttributeLong(null, XML_ATTR_PR_NEG_REGULATIONS);
+        report.numNegativeRegulations =
+                parser.getAttributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS);
+
+        return report;
+    }
+
     private void scheduleCleanup(long earliestEndTime) {
         if (earliestEndTime == Long.MAX_VALUE) {
             return;
@@ -501,6 +552,11 @@
                             writeUserLocked(out, userId));
                 }
 
+                List<Analyst.Report> reports = mAnalyst.getReports();
+                for (int i = 0, size = reports.size(); i < size; ++i) {
+                    writeReport(out, reports.get(i));
+                }
+
                 out.endTag(null, XML_TAG_TARE);
 
                 out.endDocument();
@@ -560,6 +616,24 @@
         out.endTag(null, XML_TAG_TRANSACTION);
     }
 
+    private static void writeReport(@NonNull TypedXmlSerializer out,
+            @NonNull Analyst.Report report) throws IOException {
+        out.startTag(null, XML_TAG_PERIOD_REPORT);
+        out.attributeInt(null, XML_ATTR_PR_DISCHARGE, report.cumulativeBatteryDischarge);
+        out.attributeInt(null, XML_ATTR_PR_BATTERY_LEVEL, report.currentBatteryLevel);
+        out.attributeLong(null, XML_ATTR_PR_PROFIT, report.cumulativeProfit);
+        out.attributeInt(null, XML_ATTR_PR_NUM_PROFIT, report.numProfitableActions);
+        out.attributeLong(null, XML_ATTR_PR_LOSS, report.cumulativeLoss);
+        out.attributeInt(null, XML_ATTR_PR_NUM_LOSS, report.numUnprofitableActions);
+        out.attributeLong(null, XML_ATTR_PR_REWARDS, report.cumulativeRewards);
+        out.attributeInt(null, XML_ATTR_PR_NUM_REWARDS, report.numRewards);
+        out.attributeLong(null, XML_ATTR_PR_POS_REGULATIONS, report.cumulativePositiveRegulations);
+        out.attributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS, report.numPositiveRegulations);
+        out.attributeLong(null, XML_ATTR_PR_NEG_REGULATIONS, report.cumulativeNegativeRegulations);
+        out.attributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS, report.numNegativeRegulations);
+        out.endTag(null, XML_TAG_PERIOD_REPORT);
+    }
+
     @GuardedBy("mIrs.getLock()")
     void dumpLocked(IndentingPrintWriter pw, boolean dumpAll) {
         pw.println("Ledgers:");
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 3158bd7..c6f5920 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -873,6 +873,7 @@
         String processName;
         @UnsupportedAppUsage
         ApplicationInfo appInfo;
+        String sdkSandboxClientAppVolumeUuid;
         String sdkSandboxClientAppPackage;
         @UnsupportedAppUsage
         List<ProviderInfo> providers;
@@ -1119,9 +1120,10 @@
 
         @Override
         public final void bindApplication(String processName, ApplicationInfo appInfo,
-                String sdkSandboxClientAppPackage, ProviderInfoList providerList,
-                ComponentName instrumentationName, ProfilerInfo profilerInfo,
-                Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher,
+                String sdkSandboxClientAppVolumeUuid, String sdkSandboxClientAppPackage,
+                ProviderInfoList providerList, ComponentName instrumentationName,
+                ProfilerInfo profilerInfo, Bundle instrumentationArgs,
+                IInstrumentationWatcher instrumentationWatcher,
                 IUiAutomationConnection instrumentationUiConnection, int debugMode,
                 boolean enableBinderTracking, boolean trackAllocation,
                 boolean isRestrictedBackupMode, boolean persistent, Configuration config,
@@ -1161,6 +1163,7 @@
             AppBindData data = new AppBindData();
             data.processName = processName;
             data.appInfo = appInfo;
+            data.sdkSandboxClientAppVolumeUuid = sdkSandboxClientAppVolumeUuid;
             data.sdkSandboxClientAppPackage = sdkSandboxClientAppPackage;
             data.providers = providerList.getList();
             data.instrumentationName = instrumentationName;
@@ -2561,6 +2564,11 @@
         return getPackageInfo(ai, compatInfo, null, false, true, false);
     }
 
+    private LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo,
+            boolean isSdkSandbox) {
+        return getPackageInfo(ai, compatInfo, null, false, true, false, isSdkSandbox);
+    }
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public final LoadedApk peekPackageInfo(String packageName, boolean includeCode) {
         synchronized (mResourcesManager) {
@@ -2577,11 +2585,18 @@
     private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
             ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
             boolean registerPackage) {
+        return getPackageInfo(aInfo, compatInfo, baseLoader, securityViolation, includeCode,
+                registerPackage, /*isSdkSandbox=*/false);
+    }
+
+    private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
+            ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
+            boolean registerPackage, boolean isSdkSandbox) {
         final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
         synchronized (mResourcesManager) {
             WeakReference<LoadedApk> ref;
-            if (differentUser) {
-                // Caching not supported across users
+            if (differentUser || isSdkSandbox) {
+                // Caching not supported across users and for sdk sandboxes
                 ref = null;
             } else if (includeCode) {
                 ref = mPackages.get(aInfo.packageName);
@@ -2628,8 +2643,8 @@
                         getSystemContext().mPackageInfo.getClassLoader());
             }
 
-            if (differentUser) {
-                // Caching not supported across users
+            if (differentUser || isSdkSandbox) {
+                // Caching not supported across users and for sdk sandboxes
             } else if (includeCode) {
                 mPackages.put(aInfo.packageName,
                         new WeakReference<LoadedApk>(packageInfo));
@@ -6575,9 +6590,11 @@
             mConfigurationController.applyCompatConfiguration();
         }
 
-        data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
-        if (data.sdkSandboxClientAppPackage != null) {
-            data.info.setSdkSandboxStorage(data.sdkSandboxClientAppPackage);
+        final boolean isSdkSandbox = data.sdkSandboxClientAppPackage != null;
+        data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo, isSdkSandbox);
+        if (isSdkSandbox) {
+            data.info.setSdkSandboxStorage(data.sdkSandboxClientAppVolumeUuid,
+                    data.sdkSandboxClientAppPackage);
         }
 
         if (agent != null) {
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index f4fbcce..75ad363 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -72,7 +72,7 @@
     @UnsupportedAppUsage
     void scheduleStopService(IBinder token);
     void bindApplication(in String packageName, in ApplicationInfo info,
-            in String sdkSandboxClientAppPackage,
+            in String sdkSandboxClientAppVolumeUuid, in String sdkSandboxClientAppPackage,
             in ProviderInfoList providerList, in ComponentName testName,
             in ProfilerInfo profilerInfo, in Bundle testArguments,
             IInstrumentationWatcher testWatcher, IUiAutomationConnection uiAutomationConnection,
diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl
index 7bb5d9a..36e5762 100644
--- a/core/java/android/app/ITaskStackListener.aidl
+++ b/core/java/android/app/ITaskStackListener.aidl
@@ -137,7 +137,7 @@
      * activities inside it belong to a managed profile user, and that user has just
      * been locked.
      */
-    void onTaskProfileLocked(int taskId, int userId);
+    void onTaskProfileLocked(in ActivityManager.RunningTaskInfo taskInfo);
 
     /**
      * Called when a task snapshot got updated.
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index deefea8..0849a6f 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -400,7 +400,8 @@
         mLibDir = aInfo.nativeLibraryDir;
         mDataDirFile = FileUtils.newFileOrNull(aInfo.dataDir);
         mDeviceProtectedDataDirFile = FileUtils.newFileOrNull(aInfo.deviceProtectedDataDir);
-        mCredentialProtectedDataDirFile = FileUtils.newFileOrNull(aInfo.credentialProtectedDataDir);
+        mCredentialProtectedDataDirFile = FileUtils.newFileOrNull(
+                aInfo.credentialProtectedDataDir);
 
         mSplitNames = aInfo.splitNames;
         mSplitAppDirs = aInfo.splitSourceDirs;
@@ -412,14 +413,16 @@
         }
     }
 
-    /** @hide */
-    void setSdkSandboxStorage(String sdkSandboxClientAppPackage) {
+    void setSdkSandboxStorage(@Nullable String sdkSandboxClientAppVolumeUuid,
+            String sdkSandboxClientAppPackage) {
         int userId = UserHandle.myUserId();
         mDeviceProtectedDataDirFile = Environment
-                .getDataMiscDeSharedSdkSandboxDirectory(userId, sdkSandboxClientAppPackage)
+                .getDataMiscDeSharedSdkSandboxDirectory(sdkSandboxClientAppVolumeUuid, userId,
+                        sdkSandboxClientAppPackage)
                 .getAbsoluteFile();
         mCredentialProtectedDataDirFile = Environment
-                .getDataMiscCeSharedSdkSandboxDirectory(userId, sdkSandboxClientAppPackage)
+                .getDataMiscCeSharedSdkSandboxDirectory(sdkSandboxClientAppVolumeUuid, userId,
+                        sdkSandboxClientAppPackage)
                 .getAbsoluteFile();
 
         if ((mApplicationInfo.privateFlags
diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java
index 83fe29f..774bc06 100644
--- a/core/java/android/app/TaskStackListener.java
+++ b/core/java/android/app/TaskStackListener.java
@@ -155,7 +155,7 @@
 
     @Override
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public void onTaskProfileLocked(int taskId, int userId) throws RemoteException {
+    public void onTaskProfileLocked(RunningTaskInfo taskInfo) throws RemoteException {
     }
 
     @Override
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index 022d213..5177cb4 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -478,8 +478,15 @@
     }
 
     /** {@hide} */
-    public static File getDataMiscCeSharedSdkSandboxDirectory(int userId, String packageName) {
-        return buildPath(getDataMiscCeDirectory(userId), "sdksandbox", packageName, "shared");
+    private static File getDataMiscCeDirectory(String volumeUuid, int userId) {
+        return buildPath(getDataDirectory(volumeUuid), "misc_ce", String.valueOf(userId));
+    }
+
+    /** {@hide} */
+    public static File getDataMiscCeSharedSdkSandboxDirectory(String volumeUuid, int userId,
+            String packageName) {
+        return buildPath(getDataMiscCeDirectory(volumeUuid, userId), "sdksandbox",
+                packageName, "shared");
     }
 
     /** {@hide} */
@@ -488,8 +495,15 @@
     }
 
     /** {@hide} */
-    public static File getDataMiscDeSharedSdkSandboxDirectory(int userId, String packageName) {
-        return buildPath(getDataMiscDeDirectory(userId), "sdksandbox", packageName, "shared");
+    private static File getDataMiscDeDirectory(String volumeUuid, int userId) {
+        return buildPath(getDataDirectory(volumeUuid), "misc_de", String.valueOf(userId));
+    }
+
+    /** {@hide} */
+    public static File getDataMiscDeSharedSdkSandboxDirectory(String volumeUuid, int userId,
+            String packageName) {
+        return buildPath(getDataMiscDeDirectory(volumeUuid, userId), "sdksandbox",
+                packageName, "shared");
     }
 
     private static File getDataProfilesDeDirectory(int userId) {
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index e2e9a85..a0a3b4f 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -782,6 +782,7 @@
         @Override
         public void setCurrentRootView(ViewRootImpl rootView) {
             synchronized (mH) {
+                mImeDispatcher.switchRootView(mCurRootView, rootView);
                 mCurRootView = rootView;
             }
         }
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index 8bdf233..5924844 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -25,8 +25,9 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.util.Log;
+import android.view.ViewRootImpl;
 
-import java.util.HashMap;
+import java.util.ArrayList;
 
 /**
  * A {@link OnBackInvokedDispatcher} for IME that forwards {@link OnBackInvokedCallback}
@@ -117,7 +118,7 @@
                 }
             };
 
-    private final HashMap<Integer, OnBackInvokedCallback> mImeCallbackMap = new HashMap<>();
+    private final ArrayList<ImeOnBackInvokedCallback> mImeCallbacks = new ArrayList<>();
 
     private void receive(
             int resultCode, Bundle resultData,
@@ -140,39 +141,55 @@
             int callbackId,
             @NonNull OnBackInvokedDispatcher receivingDispatcher) {
         final ImeOnBackInvokedCallback imeCallback =
-                new ImeOnBackInvokedCallback(iCallback);
-        mImeCallbackMap.put(callbackId, imeCallback);
+                new ImeOnBackInvokedCallback(iCallback, callbackId, priority);
+        mImeCallbacks.add(imeCallback);
         receivingDispatcher.registerOnBackInvokedCallback(priority, imeCallback);
     }
 
     private void unregisterReceivedCallback(
             int callbackId, OnBackInvokedDispatcher receivingDispatcher) {
-        final OnBackInvokedCallback callback = mImeCallbackMap.get(callbackId);
+        ImeOnBackInvokedCallback callback = null;
+        for (ImeOnBackInvokedCallback imeCallback : mImeCallbacks) {
+            if (imeCallback.getId() == callbackId) {
+                callback = imeCallback;
+                break;
+            }
+        }
         if (callback == null) {
             Log.e(TAG, "Ime callback not found. Ignoring unregisterReceivedCallback. "
                     + "callbackId: " + callbackId);
             return;
         }
         receivingDispatcher.unregisterOnBackInvokedCallback(callback);
+        mImeCallbacks.remove(callback);
     }
 
     /** Clears all registered callbacks on the instance. */
     public void clear() {
         // Unregister previously registered callbacks if there's any.
         if (getReceivingDispatcher() != null) {
-            for (OnBackInvokedCallback callback : mImeCallbackMap.values()) {
+            for (ImeOnBackInvokedCallback callback : mImeCallbacks) {
                 getReceivingDispatcher().unregisterOnBackInvokedCallback(callback);
             }
         }
-        mImeCallbackMap.clear();
+        mImeCallbacks.clear();
     }
 
     static class ImeOnBackInvokedCallback implements OnBackInvokedCallback {
         @NonNull
         private final IOnBackInvokedCallback mIOnBackInvokedCallback;
+        /**
+         * The hashcode of the callback instance in the IME process, used as a unique id to
+         * identify the callback when it's passed between processes.
+         */
+        private final int mId;
+        private final int mPriority;
 
-        ImeOnBackInvokedCallback(@NonNull IOnBackInvokedCallback iCallback) {
+        ImeOnBackInvokedCallback(@NonNull IOnBackInvokedCallback iCallback, int id,
+                @Priority int priority) {
             mIOnBackInvokedCallback = iCallback;
+            mId = id;
+            mPriority = priority;
         }
 
         @Override
@@ -186,8 +203,33 @@
             }
         }
 
+        private int getId() {
+            return mId;
+        }
+
         IOnBackInvokedCallback getIOnBackInvokedCallback() {
             return mIOnBackInvokedCallback;
         }
     }
+
+    /**
+     * Transfers {@link ImeOnBackInvokedCallback}s registered on one {@link ViewRootImpl} to
+     * another {@link ViewRootImpl} on focus change.
+     *
+     * @param previous the previously focused {@link ViewRootImpl}.
+     * @param current the currently focused {@link ViewRootImpl}.
+     */
+    // TODO(b/232845902): Add CTS to test IME back behavior when there's root view change while
+    // IME is up.
+    public void switchRootView(ViewRootImpl previous, ViewRootImpl current) {
+        for (ImeOnBackInvokedCallback imeCallback : mImeCallbacks) {
+            if (previous != null) {
+                previous.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(imeCallback);
+            }
+            if (current != null) {
+                current.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                        imeCallback.mPriority, imeCallback);
+            }
+        }
+    }
 }
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index edfdbcc..985e2b9 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -157,15 +157,16 @@
 
     /** Clears all registered callbacks on the instance. */
     public void clear() {
+        if (mImeDispatcher != null) {
+            mImeDispatcher.clear();
+            mImeDispatcher = null;
+        }
         if (!mAllCallbacks.isEmpty()) {
             // Clear binder references in WM.
             setTopOnBackInvokedCallback(null);
         }
         mAllCallbacks.clear();
         mOnBackInvokedCallbacks.clear();
-        if (mImeDispatcher != null) {
-            mImeDispatcher = null;
-        }
     }
 
     private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallback callback) {
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index 8276d10..0eca0a8 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -425,7 +425,7 @@
 
         @Override
         public void bindApplication(String s, ApplicationInfo applicationInfo,
-                String sdkSandboxClientAppPackage,
+                String sdkSandboxClientAppVolumeUuid, String sdkSandboxClientAppPackage,
                 ProviderInfoList list, ComponentName componentName, ProfilerInfo profilerInfo,
                 Bundle bundle, IInstrumentationWatcher iInstrumentationWatcher,
                 IUiAutomationConnection iUiAutomationConnection, int i, boolean b, boolean b1,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt
index 0d3ba9a..1eee029 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt
@@ -126,7 +126,11 @@
         @UserIdInt userId: Int,
         @UserIdInt parentUserId: Int
     ): Boolean {
-        return entitiesByUser.get(parentUserId).removeIf { b: BubbleEntity -> b.userId == userId }
+        if (entitiesByUser.get(parentUserId) != null) {
+            return entitiesByUser.get(parentUserId).removeIf {
+                b: BubbleEntity -> b.userId == userId }
+        }
+        return false
     }
 
     /**
@@ -141,8 +145,9 @@
             // First check if the user is a parent / top-level user
             val parentUserId = entitiesByUser.keyAt(i)
             if (!activeUsers.contains(parentUserId)) {
-                return removeBubblesForUser(parentUserId, -1)
-            } else {
+                entitiesByUser.remove(parentUserId)
+                return true
+            } else if (entitiesByUser.get(parentUserId) != null) {
                 // Then check if each of the bubbles in the top-level user, still has a valid user
                 // as it could belong to a profile and have a different id from the parent.
                 return entitiesByUser.get(parentUserId).removeIf { b: BubbleEntity ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java
index 59374a6..b9ddd36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java
@@ -38,7 +38,7 @@
 
     default void onTaskStackChanged() { }
 
-    default void onTaskProfileLocked(int taskId, int userId) { }
+    default void onTaskProfileLocked(RunningTaskInfo taskInfo) { }
 
     default void onTaskDisplayChanged(int taskId, int newDisplayId) { }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
index 3b67005..85e2654 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
@@ -150,8 +150,8 @@
     }
 
     @Override
-    public void onTaskProfileLocked(int taskId, int userId) {
-        mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskId, userId).sendToTarget();
+    public void onTaskProfileLocked(ActivityManager.RunningTaskInfo taskInfo) {
+        mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskInfo).sendToTarget();
     }
 
     @Override
@@ -341,8 +341,10 @@
                     break;
                 }
                 case ON_TASK_PROFILE_LOCKED: {
+                    final ActivityManager.RunningTaskInfo
+                            info = (ActivityManager.RunningTaskInfo) msg.obj;
                     for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
-                        mTaskStackListeners.get(i).onTaskProfileLocked(msg.arg1, msg.arg2);
+                        mTaskStackListeners.get(i).onTaskProfileLocked(info);
                     }
                     break;
                 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt
index 77c7055..9f0d89b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt
@@ -269,6 +269,12 @@
         assertThat(repository.getEntities(user11.identifier).toList())
                 .isEqualTo(listOf(bubble11, bubble12))
     }
+
+    @Test
+    fun testRemoveBubbleForUser_invalidInputDoesntCrash() {
+        repository.removeBubblesForUser(-1, 0)
+        repository.removeBubblesForUser(-1, -1)
+    }
 }
 
 private const val PKG_MESSENGER = "com.example.messenger"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
index d8aebc2..96938eb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
@@ -109,9 +109,10 @@
 
     @Test
     public void testOnTaskProfileLocked() {
-        mImpl.onTaskProfileLocked(1, 2);
-        verify(mCallback).onTaskProfileLocked(eq(1), eq(2));
-        verify(mOtherCallback).onTaskProfileLocked(eq(1), eq(2));
+        ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class);
+        mImpl.onTaskProfileLocked(info);
+        verify(mCallback).onTaskProfileLocked(eq(info));
+        verify(mOtherCallback).onTaskProfileLocked(eq(info));
     }
 
     @Test
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp
index 4106ac5..4757513 100644
--- a/packages/PackageInstaller/Android.bp
+++ b/packages/PackageInstaller/Android.bp
@@ -47,3 +47,39 @@
         "androidx.leanback_leanback",
     ],
 }
+
+android_app {
+    name: "PackageInstaller_tablet",
+    defaults: ["platform_app_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    certificate: "platform",
+    privileged: true,
+    platform_apis: true,
+    rename_resources_package: false,
+
+    static_libs: [
+        "xz-java",
+        "androidx.leanback_leanback",
+    ],
+    aaptflags: ["--product tablet"],
+}
+
+android_app {
+    name: "PackageInstaller_tv",
+    defaults: ["platform_app_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    certificate: "platform",
+    privileged: true,
+    platform_apis: true,
+    rename_resources_package: false,
+
+    static_libs: [
+        "xz-java",
+        "androidx.leanback_leanback",
+    ],
+    aaptflags: ["--product tv"],
+}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 4d0888a..5dd1b09 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -723,7 +723,7 @@
                   android:excludeFromRecents="true"
                   android:stateNotNeeded="true"
                   android:resumeWhilePausing="true"
-                  android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
+                  android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
             <intent-filter>
                 <action android:name="android.app.action.CONFIRM_DEVICE_CREDENTIAL_WITH_USER" />
                 <category android:name="android.intent.category.DEFAULT" />
diff --git a/packages/SystemUI/res/layout/auth_biometric_background.xml b/packages/SystemUI/res/layout/auth_biometric_background.xml
new file mode 100644
index 0000000..7ce81ad
--- /dev/null
+++ b/packages/SystemUI/res/layout/auth_biometric_background.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="80dp"
+        android:layout_height="80dp"
+        android:layout_marginBottom="160dp"/>
+
+</LinearLayout>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java
index f65d82a..acd4222 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java
@@ -61,7 +61,7 @@
         onActivityLaunchOnSecondaryDisplayRerouted();
     }
 
-    default void onTaskProfileLocked(int taskId, int userId) { }
+    default void onTaskProfileLocked(RunningTaskInfo taskInfo) { }
     default void onTaskCreated(int taskId, ComponentName componentName) { }
     default void onTaskRemoved(int taskId) { }
     default void onTaskMovedToFront(int taskId) { }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
index b5019b4..2fd5aae 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
@@ -211,8 +211,8 @@
         }
 
         @Override
-        public void onTaskProfileLocked(int taskId, int userId) {
-            mHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskId, userId).sendToTarget();
+        public void onTaskProfileLocked(RunningTaskInfo taskInfo) {
+            mHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskInfo).sendToTarget();
         }
 
         @Override
@@ -357,8 +357,9 @@
                         break;
                     }
                     case ON_TASK_PROFILE_LOCKED: {
+                        final RunningTaskInfo info = (RunningTaskInfo) msg.obj;
                         for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
-                            mTaskStackListeners.get(i).onTaskProfileLocked(msg.arg1, msg.arg2);
+                            mTaskStackListeners.get(i).onTaskProfileLocked(info);
                         }
                         break;
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
index e6b650b..546a409 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
@@ -17,23 +17,22 @@
 package com.android.systemui.keyguard;
 
 import static android.app.ActivityManager.TaskDescription;
-import static android.app.admin.DevicePolicyResources.Strings.SystemUi.WORK_LOCK_ACCESSIBILITY;
 
-import android.annotation.ColorInt;
 import android.annotation.UserIdInt;
 import android.app.Activity;
 import android.app.ActivityOptions;
 import android.app.KeyguardManager;
 import android.app.PendingIntent;
-import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.graphics.Color;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.UserHandle;
-import android.view.View;
+import android.os.UserManager;
+import android.widget.ImageView;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.R;
@@ -52,12 +51,6 @@
 public class WorkLockActivity extends Activity {
     private static final String TAG = "WorkLockActivity";
 
-    /**
-     * Contains a {@link TaskDescription} for the activity being covered.
-     */
-    static final String EXTRA_TASK_DESCRIPTION =
-            "com.android.systemui.keyguard.extra.TASK_DESCRIPTION";
-
     private static final int REQUEST_CODE_CONFIRM_CREDENTIALS = 1;
 
     /**
@@ -65,12 +58,17 @@
      * @see KeyguardManager
      */
     private KeyguardManager mKgm;
+    private UserManager mUserManager;
+    private PackageManager mPackageManager;
     private final BroadcastDispatcher mBroadcastDispatcher;
 
     @Inject
-    public WorkLockActivity(BroadcastDispatcher broadcastDispatcher) {
+    public WorkLockActivity(BroadcastDispatcher broadcastDispatcher, UserManager userManager,
+            PackageManager packageManager) {
         super();
         mBroadcastDispatcher = broadcastDispatcher;
+        mUserManager = userManager;
+        mPackageManager = packageManager;
     }
 
     @Override
@@ -91,15 +89,28 @@
         // Draw captions overlaid on the content view, so the whole window is one solid color.
         setOverlayWithDecorCaptionEnabled(true);
 
-        // Blank out the activity. When it is on-screen it will look like a Recents thumbnail with
-        // redaction switched on.
-        final DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class);
-        String contentDescription = dpm.getResources().getString(
-                WORK_LOCK_ACCESSIBILITY, () -> getString(R.string.accessibility_desc_work_lock));
-        final View blankView = new View(this);
-        blankView.setContentDescription(contentDescription);
-        blankView.setBackgroundColor(getPrimaryColor());
-        setContentView(blankView);
+        // Add background protection that contains a badged icon of the app being opened.
+        setContentView(R.layout.auth_biometric_background);
+        Drawable badgedIcon = getBadgedIcon();
+        if (badgedIcon != null) {
+            ((ImageView) findViewById(R.id.icon)).setImageDrawable(badgedIcon);
+        }
+    }
+
+    @VisibleForTesting
+    protected Drawable getBadgedIcon() {
+        String packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+        if (!packageName.isEmpty()) {
+            try {
+                return mUserManager.getBadgedIconForUser(mPackageManager.getApplicationIcon(
+                        mPackageManager.getApplicationInfoAsUser(packageName,
+                                PackageManager.ApplicationInfoFlags.of(0), getTargetUserId())),
+                        UserHandle.of(getTargetUserId()));
+            } catch (PackageManager.NameNotFoundException e) {
+                // Unable to set the badged icon, show the background protection without an icon.
+            }
+        }
+        return null;
     }
 
     /**
@@ -208,19 +219,4 @@
     final int getTargetUserId() {
         return getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
     }
-
-    @VisibleForTesting
-    @ColorInt
-    final int getPrimaryColor() {
-        final TaskDescription taskDescription = (TaskDescription)
-                getIntent().getExtra(EXTRA_TASK_DESCRIPTION);
-        if (taskDescription != null && Color.alpha(taskDescription.getPrimaryColor()) == 255) {
-            return taskDescription.getPrimaryColor();
-        } else {
-            // No task description. Use an organization color set by the policy controller.
-            final DevicePolicyManager devicePolicyManager = (DevicePolicyManager)
-                    getSystemService(Context.DEVICE_POLICY_SERVICE);
-            return devicePolicyManager.getOrganizationColorForUser(getTargetUserId());
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
index 7585110..16817ed 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
@@ -52,22 +52,17 @@
         tscl.registerTaskStackListener(mLockListener);
     }
 
-    private void startWorkChallengeInTask(int taskId, int userId) {
-        ActivityManager.TaskDescription taskDescription = null;
-        try {
-            taskDescription = mIatm.getTaskDescription(taskId);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed to get description for task=" + taskId);
-        }
+    private void startWorkChallengeInTask(ActivityManager.RunningTaskInfo info) {
+        String packageName = info.baseActivity != null ? info.baseActivity.getPackageName() : "";
         Intent intent = new Intent(KeyguardManager.ACTION_CONFIRM_DEVICE_CREDENTIAL_WITH_USER)
                 .setComponent(new ComponentName(mContext, WorkLockActivity.class))
-                .putExtra(Intent.EXTRA_USER_ID, userId)
-                .putExtra(WorkLockActivity.EXTRA_TASK_DESCRIPTION, taskDescription)
+                .putExtra(Intent.EXTRA_USER_ID, info.userId)
+                .putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
                 .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
                         | Intent.FLAG_ACTIVITY_CLEAR_TOP);
 
         final ActivityOptions options = ActivityOptions.makeBasic();
-        options.setLaunchTaskId(taskId);
+        options.setLaunchTaskId(info.taskId);
         options.setTaskOverlay(true, false /* canResume */);
 
         final int result = startActivityAsUser(intent, options.toBundle(), UserHandle.USER_CURRENT);
@@ -77,9 +72,9 @@
             // Starting the activity inside the task failed. We can't be sure why, so to be
             // safe just remove the whole task if it still exists.
             try {
-                mIatm.removeTask(taskId);
+                mIatm.removeTask(info.taskId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed to get description for task=" + taskId);
+                Log.w(TAG, "Failed to get description for task=" + info.taskId);
             }
         }
     }
@@ -112,8 +107,8 @@
 
     private final TaskStackChangeListener mLockListener = new TaskStackChangeListener() {
         @Override
-        public void onTaskProfileLocked(int taskId, int userId) {
-            startWorkChallengeInTask(taskId, userId);
+        public void onTaskProfileLocked(ActivityManager.RunningTaskInfo info) {
+            startWorkChallengeInTask(info);
         }
     };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 7800b4c..8405aea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -22,6 +22,7 @@
 import android.animation.ValueAnimator
 import android.content.Context
 import android.graphics.Rect
+import android.view.ContextThemeWrapper
 import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.View
@@ -46,6 +47,7 @@
 ) : SystemStatusAnimationCallback {
 
     private lateinit var animationWindowView: FrameLayout
+    private lateinit var themedContext: ContextThemeWrapper
 
     private var currentAnimatedView: BackgroundAnimatableView? = null
 
@@ -76,7 +78,7 @@
 
         // Initialize the animated view
         val insets = contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()
-        currentAnimatedView = viewCreator(context).also {
+        currentAnimatedView = viewCreator(themedContext).also {
             animationWindowView.addView(
                     it.view,
                     layoutParamsDefault(
@@ -221,7 +223,8 @@
 
     private fun init() {
         initialized = true
-        animationWindowView = LayoutInflater.from(context)
+        themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
+        animationWindowView = LayoutInflater.from(themedContext)
                 .inflate(R.layout.system_event_animation_window, null) as FrameLayout
         val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
         lp.gravity = Gravity.END or Gravity.CENTER_VERTICAL
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 075df1a..366cd07 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -2315,8 +2315,9 @@
     void setQsExpansion(float height) {
         height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
         mQsFullyExpanded = height == mQsMaxExpansionHeight && mQsMaxExpansionHeight != 0;
+        boolean qsAnimatingAway = !mQsAnimatorExpand && mAnimatingQS;
         if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling
-                && !mDozing) {
+                && !mDozing && !qsAnimatingAway) {
             setQsExpanded(true);
         } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
             setQsExpanded(false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
index 4c43734..576962d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
@@ -103,6 +103,10 @@
     private boolean mShowSeconds;
     private Handler mSecondsHandler;
 
+    // Fields to cache the width so the clock remains at an approximately constant width
+    private int mCharsAtCurrentWidth = -1;
+    private int mCachedWidth = -1;
+
     /**
      * Color to be set on this {@link TextView}, when wallpaperTextColor is <b>not</b> utilized.
      */
@@ -302,6 +306,32 @@
         setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime()));
     }
 
+    /**
+     * In order to avoid the clock growing and shrinking due to proportional fonts, we want to
+     * cache the drawn width at a given number of characters (removing the cache when it changes),
+     * and only use the biggest value. This means that the clock width with grow to the maximum
+     * size over time, but reset whenever the number of characters changes (or the configuration
+     * changes)
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        int chars = getText().length();
+        if (chars != mCharsAtCurrentWidth) {
+            mCharsAtCurrentWidth = chars;
+            mCachedWidth = getMeasuredWidth();
+            return;
+        }
+
+        int measuredWidth = getMeasuredWidth();
+        if (mCachedWidth > measuredWidth) {
+            setMeasuredDimension(mCachedWidth, getMeasuredHeight());
+        } else {
+            mCachedWidth = measuredWidth;
+        }
+    }
+
     @Override
     public void onTuningChanged(String key, String newValue) {
         if (CLOCK_SECONDS.equals(key)) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
index cd5740d..e9db8cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
@@ -60,6 +60,13 @@
 public class WorkLockActivityControllerTest extends SysuiTestCase {
     private static final int USER_ID = 333;
     private static final int TASK_ID = 444;
+    private static final ActivityManager.RunningTaskInfo TASK_INFO =
+            new ActivityManager.RunningTaskInfo();
+
+    static {
+        TASK_INFO.userId = USER_ID;
+        TASK_INFO.taskId = TASK_ID;
+    }
 
     private @Mock Context mContext;
     private @Mock TaskStackChangeListeners mTaskStackChangeListeners;
@@ -91,7 +98,7 @@
         setActivityStartCode(TASK_ID, true /*taskOverlay*/, ActivityManager.START_SUCCESS);
 
         // And the controller receives a message saying the profile is locked,
-        mTaskStackListener.onTaskProfileLocked(TASK_ID, USER_ID);
+        mTaskStackListener.onTaskProfileLocked(TASK_INFO);
 
         // The overlay should start and the task the activity started in should not be removed.
         verifyStartActivity(TASK_ID, true /*taskOverlay*/);
@@ -104,7 +111,7 @@
         setActivityStartCode(TASK_ID, true /*taskOverlay*/, ActivityManager.START_CLASS_NOT_FOUND);
 
         // And the controller receives a message saying the profile is locked,
-        mTaskStackListener.onTaskProfileLocked(TASK_ID, USER_ID);
+        mTaskStackListener.onTaskProfileLocked(TASK_INFO);
 
         // The task the activity started in should be removed to prevent the locked task from
         // being shown.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java
index e4c387a..640e6dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.keyguard;
 
-import static android.app.ActivityManager.TaskDescription;
-
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.eq;
@@ -25,14 +23,15 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.ColorInt;
 import android.annotation.UserIdInt;
-import android.app.KeyguardManager;
-import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.Color;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
 import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -53,18 +52,21 @@
 @RunWith(AndroidJUnit4.class)
 public class WorkLockActivityTest extends SysuiTestCase {
     private static final @UserIdInt int USER_ID = 270;
-    private static final String TASK_LABEL = "task label";
+    private static final String CALLING_PACKAGE_NAME = "com.android.test";
 
-    private @Mock DevicePolicyManager mDevicePolicyManager;
-    private @Mock KeyguardManager mKeyguardManager;
+    private @Mock UserManager mUserManager;
+    private @Mock PackageManager mPackageManager;
     private @Mock Context mContext;
     private @Mock BroadcastDispatcher mBroadcastDispatcher;
+    private @Mock Drawable mDrawable;
+    private @Mock Drawable mBadgedDrawable;
 
     private WorkLockActivity mActivity;
 
     private static class WorkLockActivityTestable extends WorkLockActivity {
-        WorkLockActivityTestable(Context baseContext, BroadcastDispatcher broadcastDispatcher) {
-            super(broadcastDispatcher);
+        WorkLockActivityTestable(Context baseContext, BroadcastDispatcher broadcastDispatcher,
+                UserManager userManager, PackageManager packageManager) {
+            super(broadcastDispatcher, userManager, packageManager);
             attachBaseContext(baseContext);
         }
     }
@@ -73,46 +75,26 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        when(mContext.getSystemService(eq(Context.DEVICE_POLICY_SERVICE)))
-                .thenReturn(mDevicePolicyManager);
-        when(mContext.getSystemService(eq(Context.KEYGUARD_SERVICE)))
-                .thenReturn(mKeyguardManager);
-
         if (Looper.myLooper() == null) {
             Looper.prepare();
         }
-        mActivity = new WorkLockActivityTestable(mContext, mBroadcastDispatcher);
+        mActivity = new WorkLockActivityTestable(mContext, mBroadcastDispatcher, mUserManager,
+                mPackageManager);
     }
 
     @Test
-    public void testBackgroundAlwaysOpaque() throws Exception {
-        final @ColorInt int orgColor = Color.rgb(250, 199, 67);
-        when(mDevicePolicyManager.getOrganizationColorForUser(eq(USER_ID))).thenReturn(orgColor);
-
-        final @ColorInt int opaqueColor= Color.rgb(164, 198, 57);
-        final @ColorInt int transparentColor = Color.argb(0, 0, 0, 0);
-        TaskDescription opaque = new TaskDescription(null, null, opaqueColor);
-        TaskDescription transparent = new TaskDescription(null, null, transparentColor);
-
-        // When a task description is provided with a suitable (opaque) primaryColor, it should be
-        // used as the scrim's background color.
+    public void testGetBadgedIcon() throws Exception {
+        ApplicationInfo info = new ApplicationInfo();
+        when(mPackageManager.getApplicationInfoAsUser(eq(CALLING_PACKAGE_NAME), any(),
+                eq(USER_ID))).thenReturn(info);
+        when(mPackageManager.getApplicationIcon(eq(info))).thenReturn(mDrawable);
+        when(mUserManager.getBadgedIconForUser(any(), eq(UserHandle.of(USER_ID)))).thenReturn(
+                mBadgedDrawable);
         mActivity.setIntent(new Intent()
                 .putExtra(Intent.EXTRA_USER_ID, USER_ID)
-                .putExtra(WorkLockActivity.EXTRA_TASK_DESCRIPTION, opaque));
-        assertEquals(opaqueColor, mActivity.getPrimaryColor());
+                .putExtra(Intent.EXTRA_PACKAGE_NAME, CALLING_PACKAGE_NAME));
 
-        // When a task description is provided but has no primaryColor / the primaryColor is
-        // transparent, the organization color should be used instead.
-        mActivity.setIntent(new Intent()
-                .putExtra(Intent.EXTRA_USER_ID, USER_ID)
-                .putExtra(WorkLockActivity.EXTRA_TASK_DESCRIPTION, transparent));
-        assertEquals(orgColor, mActivity.getPrimaryColor());
-
-        // When no task description is provided at all, it should be treated like a transparent
-        // description and the organization color shown instead.
-        mActivity.setIntent(new Intent()
-                .putExtra(Intent.EXTRA_USER_ID, USER_ID));
-        assertEquals(orgColor, mActivity.getPrimaryColor());
+        assertEquals(mBadgedDrawable, mActivity.getBadgedIcon());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ClockTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ClockTest.kt
new file mode 100644
index 0000000..22c72cc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ClockTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.statusbar.policy
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View.MeasureSpec.UNSPECIFIED
+import android.view.View.MeasureSpec.makeMeasureSpec
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Before
+import org.junit.runner.RunWith
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class ClockTest : SysuiTestCase() {
+    private lateinit var clockView: Clock
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        TestableLooper.get(this).runWithLooper {
+            val container = LinearLayout(context)
+            val lp = LinearLayout.LayoutParams(1000, WRAP_CONTENT)
+            container.layoutParams = lp
+            clockView = Clock(context, null)
+            container.addView(clockView)
+            measureClock()
+        }
+    }
+
+    @Test
+    fun testWidthDoesNotDecrease_sameCharLength() {
+        // GIVEN time is narrow
+        clockView.text = ONE_3
+        measureClock()
+        val width1 = clockView.measuredWidth
+
+        // WHEN the text changes to be wider characters
+        clockView.text = ZERO_3
+        measureClock()
+        val width2 = clockView.measuredWidth
+
+        // THEN the width should be wider (or equals when using monospace font)
+        assertThat(width2).isAtLeast(width1)
+    }
+
+    @Test
+    fun testWidthDoesNotDecrease_narrowerFont_sameNumberOfChars() {
+        // GIVEN time is wide
+        clockView.text = ZERO_3
+        measureClock()
+        val width1 = clockView.measuredWidth
+
+        // WHEN the text changes to a narrower font
+        clockView.text = ONE_3
+        measureClock()
+        val width2 = clockView.measuredWidth
+
+        // THEN the width should not have decreased, and they should in fact be the same
+        assertThat(width2).isEqualTo(width1)
+    }
+
+    @Test
+    fun testWidthIncreases_whenCharsChanges() {
+        // GIVEN wide 3-char text
+        clockView.text = ZERO_3
+        measureClock()
+        val width1 = clockView.measuredWidth
+
+        // WHEN text changes to 4-char wide text
+        clockView.text = ZERO_4
+        measureClock()
+        val width2 = clockView.measuredWidth
+
+        // THEN the text field is wider
+        assertThat(width2).isGreaterThan(width1)
+    }
+
+    @Test
+    fun testWidthDecreases_whenCharsChange_longToShort() {
+        // GIVEN wide 4-char text
+        clockView.text = ZERO_4
+        measureClock()
+        val width1 = clockView.measuredWidth
+
+        // WHEN number of characters changes to a narrow 3-char text
+        clockView.text = ONE_3
+        measureClock()
+        val width2 = clockView.measuredWidth
+
+        // THEN the width can shrink, because number of chars changed
+        assertThat(width2).isLessThan(width1)
+    }
+
+    private fun measureClock() {
+        clockView.measure(
+                makeMeasureSpec(0, UNSPECIFIED),
+                makeMeasureSpec(0, UNSPECIFIED)
+        )
+    }
+}
+
+/**
+ * In a non-monospace font, it is expected that "0:00" is wider than "1:11"
+ */
+private const val ZERO_3 = "0:00"
+private const val ZERO_4 = "00:00"
+private const val ONE_3 = "1:11"
+private const val ONE_4 = "11:11"
diff --git a/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java b/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java
index 2eae6af..222d779 100644
--- a/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java
+++ b/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java
@@ -147,7 +147,10 @@
                 }
             });
             if (sessionInfo.linkToDeath()) {
-                mCallbackQueue.put(requestId, sessionInfo);
+                CloudSearchCallbackInfo removedInfo = mCallbackQueue.put(requestId, sessionInfo);
+                if (removedInfo != null) {
+                    removedInfo.destroy();
+                }
             } else {
                 // destroy the session if calling process is already dead
                 onDestroyLocked(requestId);
diff --git a/services/core/java/com/android/server/CircularQueue.java b/services/core/java/com/android/server/CircularQueue.java
index aac6752..4538078 100644
--- a/services/core/java/com/android/server/CircularQueue.java
+++ b/services/core/java/com/android/server/CircularQueue.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import android.annotation.Nullable;
 import android.util.ArrayMap;
 
 import java.util.Collection;
@@ -43,16 +44,19 @@
     /**
      * Put a (key|value) pair in the CircularQueue. Only the key will be added to the queue. Value
      * will be added to the ArrayMap.
-     * @return {@code true} (as specified by {@link Collection#add})
+     * @return the most recently removed value if keys were removed, or {@code null} if no keys were
+     * removed.
      */
-    public boolean put(K key, V value) {
+    @Nullable
+    public V put(K key, V value) {
         super.add(key);
         mArrayMap.put(key, value);
+        V removedValue = null;
         while (size() > mLimit) {
             K removedKey = super.remove();
-            mArrayMap.remove(removedKey);
+            removedValue = mArrayMap.remove(removedKey);
         }
-        return true;
+        return removedValue;
     }
 
     /**
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 163df10..3a5a866 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4859,7 +4859,8 @@
                 thread.runIsolatedEntryPoint(
                         app.getIsolatedEntryPoint(), app.getIsolatedEntryPointArgs());
             } else if (instr2 != null) {
-                thread.bindApplication(processName, appInfo, app.sdkSandboxClientAppPackage,
+                thread.bindApplication(processName, appInfo,
+                        app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
                         providerList,
                         instr2.mClass,
                         profilerInfo, instr2.mArguments,
@@ -4874,7 +4875,8 @@
                         app.getDisabledCompatChanges(), serializedSystemFontMap,
                         app.getStartElapsedTime(), app.getStartUptime());
             } else {
-                thread.bindApplication(processName, appInfo, app.sdkSandboxClientAppPackage,
+                thread.bindApplication(processName, appInfo,
+                        app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
                         providerList, null, profilerInfo, null, null, null, testMode,
                         mBinderTransactionTrackingEnabled, enableTrackAllocation,
                         isRestrictedBackupMode || !normalMode, app.isPersistent(),
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index b4ff870..ac5cb2d 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -87,6 +87,7 @@
     final String processName;   // name of the process
     final String sdkSandboxClientAppPackage; // if this is an sdk sandbox process, name of the
                                              // app package for which it is running
+    final String sdkSandboxClientAppVolumeUuid; // uuid of the app for which the sandbox is running
 
     /**
      * Overall state of process's uid.
@@ -535,6 +536,11 @@
         userId = UserHandle.getUserId(_uid);
         processName = _processName;
         sdkSandboxClientAppPackage = _sdkSandboxClientAppPackage;
+        if (isSdkSandbox) {
+            sdkSandboxClientAppVolumeUuid = getClientInfoForSdkSandbox().volumeUuid;
+        } else {
+            sdkSandboxClientAppVolumeUuid = null;
+        }
         mPersistent = false;
         mRemoved = false;
         mProfile = new ProcessProfileRecord(this);
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 3f00244..61350bb 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -380,12 +380,12 @@
             }
             checkDataOwner(clip, intendingUid);
             synchronized (mLock) {
-                scheduleAutoClear(userId);
+                scheduleAutoClear(userId, intendingUid);
                 setPrimaryClipInternalLocked(clip, intendingUid, sourcePackage);
             }
         }
 
-        private void scheduleAutoClear(@UserIdInt int userId) {
+        private void scheduleAutoClear(@UserIdInt int userId, int intendingUid) {
             final long oldIdentity = Binder.clearCallingIdentity();
             try {
                 if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CLIPBOARD,
@@ -393,7 +393,7 @@
                     mClipboardClearHandler.removeEqualMessages(ClipboardClearHandler.MSG_CLEAR,
                             userId);
                     Message clearMessage = Message.obtain(mClipboardClearHandler,
-                            ClipboardClearHandler.MSG_CLEAR, userId, 0, userId);
+                            ClipboardClearHandler.MSG_CLEAR, userId, intendingUid, userId);
                     mClipboardClearHandler.sendMessageDelayed(clearMessage,
                             getTimeoutForAutoClear());
                 }
@@ -446,7 +446,7 @@
                 showAccessNotificationLocked(pkg, intendingUid, intendingUserId, clipboard);
                 notifyTextClassifierLocked(clipboard, pkg, intendingUid);
                 if (clipboard.primaryClip != null) {
-                    scheduleAutoClear(userId);
+                    scheduleAutoClear(userId, intendingUid);
                 }
                 return clipboard.primaryClip;
             }
@@ -554,11 +554,12 @@
                 switch (msg.what) {
                     case MSG_CLEAR:
                         final int userId = msg.arg1;
+                        final int intendingUid = msg.arg2;
                         synchronized (mLock) {
                             if (getClipboardLocked(userId).primaryClip != null) {
                                 FrameworkStatsLog.write(FrameworkStatsLog.CLIPBOARD_CLEARED,
                                         FrameworkStatsLog.CLIPBOARD_CLEARED__SOURCE__AUTO_CLEAR);
-                                setPrimaryClipInternalLocked(null, Binder.getCallingUid(), null);
+                                setPrimaryClipInternalLocked(null, intendingUid, null);
                             }
                         }
                         break;
diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java
index 954b930..f93e06d 100644
--- a/services/core/java/com/android/server/content/ContentService.java
+++ b/services/core/java/com/android/server/content/ContentService.java
@@ -78,6 +78,7 @@
 import com.android.internal.os.BinderDeathDispatcher;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -1542,11 +1543,62 @@
             return ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET_WITH_TEMP;
         }
         if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND || isUidActive) {
+            FrameworkStatsLog.write(FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED,
+                    callingUid, getProcStateForStatsd(procState), isUidActive);
             return ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET;
         }
         return ContentResolver.SYNC_EXEMPTION_NONE;
     }
 
+    private int getProcStateForStatsd(int procState) {
+        switch (procState) {
+            case ActivityManager.PROCESS_STATE_UNKNOWN:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__UNKNOWN;
+            case ActivityManager.PROCESS_STATE_PERSISTENT:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__PERSISTENT;
+            case ActivityManager.PROCESS_STATE_PERSISTENT_UI:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__PERSISTENT_UI;
+            case ActivityManager.PROCESS_STATE_TOP:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__TOP;
+            case ActivityManager.PROCESS_STATE_BOUND_TOP:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__BOUND_TOP;
+            case ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__FOREGROUND_SERVICE;
+            case ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE:
+                return FrameworkStatsLog
+                        .SYNC_EXEMPTION_OCCURRED__PROC_STATE__BOUND_FOREGROUND_SERVICE;
+            case ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__IMPORTANT_FOREGROUND;
+            case ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__IMPORTANT_BACKGROUND;
+            case ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__TRANSIENT_BACKGROUND;
+            case ActivityManager.PROCESS_STATE_BACKUP:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__BACKUP;
+            case ActivityManager.PROCESS_STATE_SERVICE:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__SERVICE;
+            case ActivityManager.PROCESS_STATE_RECEIVER:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__RECEIVER;
+            case ActivityManager.PROCESS_STATE_TOP_SLEEPING:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__TOP_SLEEPING;
+            case ActivityManager.PROCESS_STATE_HEAVY_WEIGHT:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__HEAVY_WEIGHT;
+            case ActivityManager.PROCESS_STATE_LAST_ACTIVITY:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__LAST_ACTIVITY;
+            case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__CACHED_ACTIVITY;
+            case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
+                return FrameworkStatsLog
+                        .SYNC_EXEMPTION_OCCURRED__PROC_STATE__CACHED_ACTIVITY_CLIENT;
+            case ActivityManager.PROCESS_STATE_CACHED_RECENT:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__CACHED_RECENT;
+            case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__CACHED_EMPTY;
+            default:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__UNKNOWN;
+        }
+    }
+
     /** {@hide} */
     @VisibleForTesting
     public static final class ObserverNode {
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/PackageInfoWithoutStateUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/PackageInfoWithoutStateUtils.java
index 91d2010..b7fde43 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/PackageInfoWithoutStateUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/PackageInfoWithoutStateUtils.java
@@ -546,7 +546,9 @@
         ai.windowLayout = a.getWindowLayout();
         ai.attributionTags = a.getAttributionTags();
         if ((flags & PackageManager.GET_META_DATA) != 0) {
-            ai.metaData = a.getMetaData();
+            var metaData = a.getMetaData();
+            // Backwards compatibility, coerce to null if empty
+            ai.metaData = metaData.isEmpty() ? null : metaData;
         }
         ai.applicationInfo = applicationInfo;
         ai.setKnownActivityEmbeddingCerts(a.getKnownActivityEmbeddingCerts());
@@ -599,7 +601,9 @@
         si.mForegroundServiceType = s.getForegroundServiceType();
         si.applicationInfo = applicationInfo;
         if ((flags & PackageManager.GET_META_DATA) != 0) {
-            si.metaData = s.getMetaData();
+            var metaData = s.getMetaData();
+            // Backwards compatibility, coerce to null if empty
+            si.metaData = metaData.isEmpty() ? null : metaData;
         }
         return si;
     }
@@ -660,7 +664,9 @@
             pi.uriPermissionPatterns = null;
         }
         if ((flags & PackageManager.GET_META_DATA) != 0) {
-            pi.metaData = p.getMetaData();
+            var metaData = p.getMetaData();
+            // Backwards compatibility, coerce to null if empty
+            pi.metaData = metaData.isEmpty() ? null : metaData;
         }
         pi.applicationInfo = applicationInfo;
         return pi;
@@ -706,7 +712,9 @@
         if ((flags & PackageManager.GET_META_DATA) == 0) {
             return ii;
         }
-        ii.metaData = i.getMetaData();
+        var metaData = i.getMetaData();
+        // Backwards compatibility, coerce to null if empty
+        ii.metaData = metaData.isEmpty() ? null : metaData;
         return ii;
     }
 
@@ -729,7 +737,9 @@
         if ((flags & PackageManager.GET_META_DATA) == 0) {
             return pi;
         }
-        pi.metaData = p.getMetaData();
+        var metaData = p.getMetaData();
+        // Backwards compatibility, coerce to null if empty
+        pi.metaData = metaData.isEmpty() ? null : metaData;
         return pi;
     }
 
@@ -753,7 +763,9 @@
         if ((flags & PackageManager.GET_META_DATA) == 0) {
             return pgi;
         }
-        pgi.metaData = pg.getMetaData();
+        var metaData = pg.getMetaData();
+        // Backwards compatibility, coerce to null if empty
+        pgi.metaData = metaData.isEmpty() ? null : metaData;
         return pgi;
     }
 
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 0aab186..7240fd5 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -3231,7 +3231,7 @@
             if (task.getActivity(activity -> !activity.finishing && activity.mUserId == userId)
                     != null) {
                 mService.getTaskChangeNotificationController().notifyTaskProfileLocked(
-                        task.mTaskId, userId);
+                        task.getTaskInfo());
             }
         }, true /* traverseTopToBottom */);
     }
diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
index 61963c4..49d064f 100644
--- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
+++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
@@ -144,7 +144,7 @@
     };
 
     private final TaskStackConsumer mNotifyTaskProfileLocked = (l, m) -> {
-        l.onTaskProfileLocked(m.arg1, m.arg2);
+        l.onTaskProfileLocked((RunningTaskInfo) m.obj);
     };
 
     private final TaskStackConsumer mNotifyTaskSnapshotChanged = (l, m) -> {
@@ -467,9 +467,9 @@
      * Notify listeners that the task has been put in a locked state because one or more of the
      * activities inside it belong to a managed profile user that has been locked.
      */
-    void notifyTaskProfileLocked(int taskId, int userId) {
-        final Message msg = mHandler.obtainMessage(NOTIFY_TASK_PROFILE_LOCKED_LISTENERS_MSG, taskId,
-                userId);
+    void notifyTaskProfileLocked(ActivityManager.RunningTaskInfo taskInfo) {
+        final Message msg = mHandler.obtainMessage(NOTIFY_TASK_PROFILE_LOCKED_LISTENERS_MSG,
+                taskInfo);
         forAllLocalListeners(mNotifyTaskProfileLocked, msg);
         msg.sendToTarget();
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java
index fac37fd..01bafc1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java
@@ -43,11 +43,12 @@
         mQueue = new CircularQueue<>(LIMIT);
         mQueue.put(1, "A");
         mQueue.put(2, "B");
-        mQueue.put(3, "C");
+        String removedElement = mQueue.put(3, "C");
         assertNull(mQueue.getElement(1));
         assertEquals(mQueue.getElement(2), "B");
         assertEquals(mQueue.getElement(3), "C");
-
+        // Confirming that put is returning the deleted element
+        assertEquals(removedElement, "A");
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
index 534d0a1..f9f6fe9 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
@@ -46,6 +46,8 @@
     @Mock
     private CompleteEconomicPolicy mEconomicPolicy;
     @Mock
+    private Analyst mAnalyst;
+    @Mock
     private Context mContext;
     @Mock
     private InternalResourceService mIrs;
@@ -53,8 +55,8 @@
     private Scribe mScribe;
 
     private static class MockScribe extends Scribe {
-        MockScribe(InternalResourceService irs) {
-            super(irs);
+        MockScribe(InternalResourceService irs, Analyst analyst) {
+            super(irs, analyst);
         }
 
         @Override
@@ -74,7 +76,7 @@
         doReturn(mEconomicPolicy).when(mIrs).getCompleteEconomicPolicyLocked();
         doReturn(mIrs).when(mIrs).getLock();
         doReturn(mock(AlarmManager.class)).when(mContext).getSystemService(Context.ALARM_SERVICE);
-        mScribe = new MockScribe(mIrs);
+        mScribe = new MockScribe(mIrs, mAnalyst);
     }
 
     @After
@@ -86,7 +88,7 @@
 
     @Test
     public void testRecordTransaction_UnderMax() {
-        Agent agent = new Agent(mIrs, mScribe);
+        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
         Ledger ledger = new Ledger();
 
         doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked();
@@ -115,7 +117,7 @@
 
     @Test
     public void testRecordTransaction_MaxConsumptionLimit() {
-        Agent agent = new Agent(mIrs, mScribe);
+        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
         Ledger ledger = new Ledger();
 
         doReturn(1000L).when(mIrs).getConsumptionLimitLocked();
@@ -162,7 +164,7 @@
 
     @Test
     public void testRecordTransaction_MaxSatiatedBalance() {
-        Agent agent = new Agent(mIrs, mScribe);
+        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
         Ledger ledger = new Ledger();
 
         doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked();
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
index 14f95e9..721777c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
@@ -16,7 +16,7 @@
 
 package com.android.server.tare;
 
-
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 
 import static org.junit.Assert.assertEquals;
@@ -41,6 +41,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
@@ -66,8 +68,11 @@
     private Scribe mScribeUnderTest;
     private File mTestFileDir;
     private final List<PackageInfo> mInstalledPackages = new ArrayList<>();
+    private final List<Analyst.Report> mReports = new ArrayList<>();
 
     @Mock
+    private Analyst mAnalyst;
+    @Mock
     private InternalResourceService mIrs;
 
     private Context getContext() {
@@ -84,11 +89,12 @@
         when(mIrs.getLock()).thenReturn(new Object());
         when(mIrs.isEnabled()).thenReturn(true);
         when(mIrs.getInstalledPackages()).thenReturn(mInstalledPackages);
+        when(mAnalyst.getReports()).thenReturn(mReports);
         mTestFileDir = new File(getContext().getFilesDir(), "scribe_test");
         //noinspection ResultOfMethodCallIgnored
         mTestFileDir.mkdirs();
         Log.d(TAG, "Saving data to '" + mTestFileDir + "'");
-        mScribeUnderTest = new Scribe(mIrs, mTestFileDir);
+        mScribeUnderTest = new Scribe(mIrs, mAnalyst, mTestFileDir);
 
         addInstalledPackage(TEST_USER_ID, TEST_PACKAGE);
     }
@@ -105,6 +111,62 @@
     }
 
     @Test
+    public void testWritingAnalystReportsToDisk() {
+        ArgumentCaptor<List<Analyst.Report>> reportCaptor =
+                ArgumentCaptor.forClass(List.class);
+
+        InOrder inOrder = inOrder(mAnalyst);
+
+        // Empty set
+        mReports.clear();
+        mScribeUnderTest.writeImmediatelyForTesting();
+        mScribeUnderTest.loadFromDiskLocked();
+        inOrder.verify(mAnalyst).loadReports(reportCaptor.capture());
+        List<Analyst.Report> result = reportCaptor.getValue();
+        assertReportListsEqual(mReports, result);
+
+        Analyst.Report report1 = new Analyst.Report();
+        report1.cumulativeBatteryDischarge = 1;
+        report1.currentBatteryLevel = 2;
+        report1.cumulativeProfit = 3;
+        report1.numProfitableActions = 4;
+        report1.cumulativeLoss = 5;
+        report1.numUnprofitableActions = 6;
+        report1.cumulativeRewards = 7;
+        report1.numRewards = 8;
+        report1.cumulativePositiveRegulations = 9;
+        report1.numPositiveRegulations = 10;
+        report1.cumulativeNegativeRegulations = 11;
+        report1.numNegativeRegulations = 12;
+        mReports.add(report1);
+        mScribeUnderTest.writeImmediatelyForTesting();
+        mScribeUnderTest.loadFromDiskLocked();
+        inOrder.verify(mAnalyst).loadReports(reportCaptor.capture());
+        result = reportCaptor.getValue();
+        assertReportListsEqual(mReports, result);
+
+        Analyst.Report report2 = new Analyst.Report();
+        report2.cumulativeBatteryDischarge = 10;
+        report2.currentBatteryLevel = 20;
+        report2.cumulativeProfit = 30;
+        report2.numProfitableActions = 40;
+        report2.cumulativeLoss = 50;
+        report2.numUnprofitableActions = 60;
+        report2.cumulativeRewards = 70;
+        report2.numRewards = 80;
+        report2.cumulativePositiveRegulations = 90;
+        report2.numPositiveRegulations = 100;
+        report2.cumulativeNegativeRegulations = 110;
+        report2.numNegativeRegulations = 120;
+        mReports.add(report2);
+        mScribeUnderTest.writeImmediatelyForTesting();
+        mScribeUnderTest.loadFromDiskLocked();
+        inOrder.verify(mAnalyst).loadReports(reportCaptor.capture());
+        result = reportCaptor.getValue();
+        assertReportListsEqual(mReports, result);
+    }
+
+    @Test
     public void testWriteHighLevelStateToDisk() {
         long lastReclamationTime = System.currentTimeMillis();
         long remainingConsumableCakes = 2000L;
@@ -277,6 +339,49 @@
         }
     }
 
+    private void assertReportListsEqual(List<Analyst.Report> expected,
+            List<Analyst.Report> actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        assertEquals(expected.size(), actual.size());
+        for (int i = 0; i < expected.size(); ++i) {
+            Analyst.Report eReport = expected.get(i);
+            Analyst.Report aReport = actual.get(i);
+            if (eReport == null) {
+                assertNull(aReport);
+                continue;
+            }
+            assertNotNull(aReport);
+            assertEquals("Reports #" + i + " cumulativeBatteryDischarge are not equal",
+                    eReport.cumulativeBatteryDischarge, aReport.cumulativeBatteryDischarge);
+            assertEquals("Reports #" + i + " currentBatteryLevel are not equal",
+                    eReport.currentBatteryLevel, aReport.currentBatteryLevel);
+            assertEquals("Reports #" + i + " cumulativeProfit are not equal",
+                    eReport.cumulativeProfit, aReport.cumulativeProfit);
+            assertEquals("Reports #" + i + " numProfitableActions are not equal",
+                    eReport.numProfitableActions, aReport.numProfitableActions);
+            assertEquals("Reports #" + i + " cumulativeLoss are not equal",
+                    eReport.cumulativeLoss, aReport.cumulativeLoss);
+            assertEquals("Reports #" + i + " numUnprofitableActions are not equal",
+                    eReport.numUnprofitableActions, aReport.numUnprofitableActions);
+            assertEquals("Reports #" + i + " cumulativeRewards are not equal",
+                    eReport.cumulativeRewards, aReport.cumulativeRewards);
+            assertEquals("Reports #" + i + " numRewards are not equal",
+                    eReport.numRewards, aReport.numRewards);
+            assertEquals("Reports #" + i + " cumulativePositiveRegulations are not equal",
+                    eReport.cumulativePositiveRegulations, aReport.cumulativePositiveRegulations);
+            assertEquals("Reports #" + i + " numPositiveRegulations are not equal",
+                    eReport.numPositiveRegulations, aReport.numPositiveRegulations);
+            assertEquals("Reports #" + i + " cumulativeNegativeRegulations are not equal",
+                    eReport.cumulativeNegativeRegulations, aReport.cumulativeNegativeRegulations);
+            assertEquals("Reports #" + i + " numNegativeRegulations are not equal",
+                    eReport.numNegativeRegulations, aReport.numNegativeRegulations);
+        }
+    }
+
     private void assertTransactionsEqual(Ledger.Transaction expected, Ledger.Transaction actual) {
         if (expected == null) {
             assertNull(actual);
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
index 952200d..2fd7853 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -65,6 +66,7 @@
 import com.android.server.pm.parsing.pkg.PackageImpl;
 import com.android.server.pm.parsing.pkg.ParsedPackage;
 import com.android.server.pm.permission.CompatibilityPermissionInfo;
+import com.android.server.pm.pkg.PackageUserState;
 import com.android.server.pm.pkg.PackageUserStateInternal;
 import com.android.server.pm.pkg.component.ParsedActivity;
 import com.android.server.pm.pkg.component.ParsedActivityImpl;
@@ -85,6 +87,7 @@
 import com.android.server.pm.pkg.component.ParsedServiceImpl;
 import com.android.server.pm.pkg.component.ParsedUsesPermission;
 import com.android.server.pm.pkg.component.ParsedUsesPermissionImpl;
+import com.android.server.pm.pkg.parsing.PackageInfoWithoutStateUtils;
 import com.android.server.pm.pkg.parsing.ParsingPackage;
 
 import org.junit.Before;
@@ -596,6 +599,38 @@
         }
     }
 
+    @Test
+    public void testNoComponentMetadataIsCoercedToNullForInfoObject() throws Exception {
+        final File testFile = extractFile(TEST_APP4_APK);
+        try {
+            final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
+            ApplicationInfo appInfo = PackageInfoWithoutStateUtils.generateApplicationInfo(pkg, 0,
+                    PackageUserState.DEFAULT, 0);
+            for (ParsedActivity activity : pkg.getActivities()) {
+                assertNotNull(activity.getMetaData());
+                assertNull(PackageInfoWithoutStateUtils.generateActivityInfoUnchecked(activity, 0,
+                        appInfo).metaData);
+            }
+            for (ParsedProvider provider : pkg.getProviders()) {
+                assertNotNull(provider.getMetaData());
+                assertNull(PackageInfoWithoutStateUtils.generateProviderInfoUnchecked(provider, 0,
+                        appInfo).metaData);
+            }
+            for (ParsedActivity receiver : pkg.getReceivers()) {
+                assertNotNull(receiver.getMetaData());
+                assertNull(PackageInfoWithoutStateUtils.generateActivityInfoUnchecked(receiver, 0,
+                        appInfo).metaData);
+            }
+            for (ParsedService service : pkg.getServices()) {
+                assertNotNull(service.getMetaData());
+                assertNull(PackageInfoWithoutStateUtils.generateServiceInfoUnchecked(service, 0,
+                        appInfo).metaData);
+            }
+        } finally {
+            testFile.delete();
+        }
+    }
+
     /**
      * A trivial subclass of package parser that only caches the package name, and throws away
      * all other information.
diff --git a/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java b/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java
new file mode 100644
index 0000000..2b527a2
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.tare;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Test that the Analyst processes transactions correctly. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class AnalystTest {
+
+    @Test
+    public void testInitialState() {
+        final Analyst analyst = new Analyst();
+        assertEquals(0, analyst.getReports().size());
+    }
+
+    @Test
+    public void testBatteryLevelChange() {
+        final Analyst analyst = new Analyst();
+
+        Analyst.Report expected = new Analyst.Report();
+        expected.currentBatteryLevel = 55;
+        analyst.noteBatteryLevelChange(55);
+        assertEquals(1, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+
+        // Discharging
+        analyst.noteBatteryLevelChange(54);
+        expected.currentBatteryLevel = 54;
+        expected.cumulativeBatteryDischarge = 1;
+        assertEquals(1, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+        analyst.noteBatteryLevelChange(50);
+        expected.currentBatteryLevel = 50;
+        expected.cumulativeBatteryDischarge = 5;
+        assertEquals(1, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+
+        // Charging
+        analyst.noteBatteryLevelChange(51);
+        expected.currentBatteryLevel = 51;
+        assertEquals(1, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+        analyst.noteBatteryLevelChange(55);
+        expected.currentBatteryLevel = 55;
+        assertEquals(1, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+
+        // Reset
+        analyst.noteBatteryLevelChange(100);
+        assertEquals(2, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+        expected.currentBatteryLevel = 100;
+        expected.cumulativeBatteryDischarge = 0;
+        assertReportsEqual(expected, analyst.getReports().get(1));
+    }
+
+    @Test
+    public void testTransaction() {
+        runTestTransactions(new Analyst(), new Analyst.Report(), 1);
+    }
+
+    @Test
+    public void testTransaction_PeriodChange() {
+        final Analyst analyst = new Analyst();
+
+        Analyst.Report expected = new Analyst.Report();
+        expected.currentBatteryLevel = 55;
+        analyst.noteBatteryLevelChange(55);
+
+        runTestTransactions(analyst, expected, 1);
+
+        expected.currentBatteryLevel = 49;
+        expected.cumulativeBatteryDischarge = 6;
+        analyst.noteBatteryLevelChange(49);
+
+        runTestTransactions(analyst, expected, 1);
+
+        expected = new Analyst.Report();
+        expected.currentBatteryLevel = 100;
+        analyst.noteBatteryLevelChange(100);
+        expected.cumulativeBatteryDischarge = 0;
+
+        runTestTransactions(analyst, expected, 2);
+    }
+
+    private void runTestTransactions(Analyst analyst, Analyst.Report lastExpectedReport,
+            int numExpectedReports) {
+        Analyst.Report expected = lastExpectedReport;
+
+        // Profit
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_ACTION, null, -51, 1));
+        expected.cumulativeProfit += 50;
+        expected.numProfitableActions += 1;
+        assertEquals(numExpectedReports, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1));
+
+        // Loss
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_ACTION, null, -51, 100));
+        expected.cumulativeLoss += 49;
+        expected.numUnprofitableActions += 1;
+        assertEquals(numExpectedReports, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1));
+
+        // Reward
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD, null, 51, 0));
+        expected.cumulativeRewards += 51;
+        expected.numRewards += 1;
+        assertEquals(numExpectedReports, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1));
+
+        // Regulations
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION, null, 25, 0));
+        expected.cumulativePositiveRegulations += 25;
+        expected.numPositiveRegulations += 1;
+        assertEquals(numExpectedReports, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1));
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION, null, -25, 0));
+        expected.cumulativeNegativeRegulations += 25;
+        expected.numNegativeRegulations += 1;
+        assertEquals(numExpectedReports, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1));
+
+        // No-ops
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_ACTION, null, -100, 100));
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION, null, 0, 0));
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD, null, 0, 0));
+        assertEquals(numExpectedReports, analyst.getReports().size());
+    }
+
+    @Test
+    public void testLoadReports() {
+        final Analyst analyst = new Analyst();
+
+        List<Analyst.Report> expected = new ArrayList<>();
+        analyst.loadReports(expected);
+        assertReportListsEqual(expected, analyst.getReports());
+
+        Analyst.Report report1 = new Analyst.Report();
+        report1.cumulativeBatteryDischarge = 1;
+        report1.currentBatteryLevel = 2;
+        report1.cumulativeProfit = 3;
+        report1.numProfitableActions = 4;
+        report1.cumulativeLoss = 5;
+        report1.numUnprofitableActions = 6;
+        report1.cumulativeRewards = 7;
+        report1.numRewards = 8;
+        report1.cumulativePositiveRegulations = 9;
+        report1.numPositiveRegulations = 10;
+        report1.cumulativeNegativeRegulations = 11;
+        report1.numNegativeRegulations = 12;
+        expected.add(report1);
+        analyst.loadReports(expected);
+        assertReportListsEqual(expected, analyst.getReports());
+
+        Analyst.Report report2 = new Analyst.Report();
+        report2.cumulativeBatteryDischarge = 10;
+        report2.currentBatteryLevel = 20;
+        report2.cumulativeProfit = 30;
+        report2.numProfitableActions = 40;
+        report2.cumulativeLoss = 50;
+        report2.numUnprofitableActions = 60;
+        report2.cumulativeRewards = 70;
+        report2.numRewards = 80;
+        report2.cumulativePositiveRegulations = 90;
+        report2.numPositiveRegulations = 100;
+        report2.cumulativeNegativeRegulations = 110;
+        report2.numNegativeRegulations = 120;
+        expected.add(report2);
+        analyst.loadReports(expected);
+        assertReportListsEqual(expected, analyst.getReports());
+    }
+
+    private void assertReportsEqual(Analyst.Report expected, Analyst.Report actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        assertEquals(expected.cumulativeBatteryDischarge, actual.cumulativeBatteryDischarge);
+        assertEquals(expected.currentBatteryLevel, actual.currentBatteryLevel);
+        assertEquals(expected.cumulativeProfit, actual.cumulativeProfit);
+        assertEquals(expected.numProfitableActions, actual.numProfitableActions);
+        assertEquals(expected.cumulativeLoss, actual.cumulativeLoss);
+        assertEquals(expected.numUnprofitableActions, actual.numUnprofitableActions);
+        assertEquals(expected.cumulativeRewards, actual.cumulativeRewards);
+        assertEquals(expected.numRewards, actual.numRewards);
+        assertEquals(expected.cumulativePositiveRegulations, actual.cumulativePositiveRegulations);
+        assertEquals(expected.numPositiveRegulations, actual.numPositiveRegulations);
+        assertEquals(expected.cumulativeNegativeRegulations, actual.cumulativeNegativeRegulations);
+        assertEquals(expected.numNegativeRegulations, actual.numNegativeRegulations);
+    }
+
+    private void assertReportListsEqual(List<Analyst.Report> expected,
+            List<Analyst.Report> actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        assertEquals(expected.size(), actual.size());
+        for (int i = 0; i < expected.size(); ++i) {
+            assertReportsEqual(expected.get(i), actual.get(i));
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 8ef9ada..700fadd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -1097,7 +1097,7 @@
         TaskChangeNotificationController controller = mAtm.getTaskChangeNotificationController();
         spyOn(controller);
         mWm.mRoot.lockAllProfileTasks(profileUserId);
-        verify(controller).notifyTaskProfileLocked(eq(task.mTaskId), eq(profileUserId));
+        verify(controller).notifyTaskProfileLocked(any());
 
         // Create the work lock activity on top of the task
         final ActivityRecord workLockActivity = new ActivityBuilder(mAtm).setTask(task).build();
@@ -1107,7 +1107,7 @@
         // Make sure the listener won't be notified again.
         clearInvocations(controller);
         mWm.mRoot.lockAllProfileTasks(profileUserId);
-        verify(controller, never()).notifyTaskProfileLocked(anyInt(), anyInt());
+        verify(controller, never()).notifyTaskProfileLocked(any());
     }
 
     /**