Merge "Similar to the create flow, make get request two staged."
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index ad6eff0..e0df22c 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -266,6 +266,8 @@
     private final List<RestrictingController> mRestrictiveControllers;
     /** Need direct access to this for testing. */
     private final StorageController mStorageController;
+    /** Needed to get estimated transfer time. */
+    private final ConnectivityController mConnectivityController;
     /** Need directly for sending uid state changes */
     private final DeviceIdleJobsController mDeviceIdleJobsController;
     /** Needed to get next estimated launch time. */
@@ -464,6 +466,13 @@
                         case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS:
                         case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS:
                         case Constants.KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS:
+                        case Constants.KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS:
+                        case Constants.KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS:
+                        case Constants.KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS:
+                        case Constants.KEY_RUNTIME_USER_INITIATED_LIMIT_MS:
+                        case Constants.KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR:
+                        case Constants.KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS:
+                        case Constants.KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS:
                             if (!runtimeUpdated) {
                                 mConstants.updateRuntimeConstantsLocked();
                                 runtimeUpdated = true;
@@ -555,6 +564,21 @@
         private static final String KEY_RUNTIME_MIN_EJ_GUARANTEE_MS = "runtime_min_ej_guarantee_ms";
         private static final String KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS =
                 "runtime_min_high_priority_guarantee_ms";
+        private static final String KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS =
+                "runtime_min_data_transfer_guarantee_ms";
+        private static final String KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS =
+                "runtime_data_transfer_limit_ms";
+        private static final String KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS =
+                "runtime_min_user_initiated_guarantee_ms";
+        private static final String KEY_RUNTIME_USER_INITIATED_LIMIT_MS =
+                "runtime_user_initiated_limit_ms";
+        private static final String
+                KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR =
+                "runtime_min_user_initiated_data_transfer_guarantee_buffer_factor";
+        private static final String KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS =
+                "runtime_min_user_initiated_data_transfer_guarantee_ms";
+        private static final String KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS =
+                "runtime_user_initiated_data_transfer_limit_ms";
 
         private static final String KEY_PERSIST_IN_SPLIT_FILES = "persist_in_split_files";
 
@@ -584,6 +608,20 @@
         public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS;
         @VisibleForTesting
         static final long DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = 5 * MINUTE_IN_MILLIS;
+        public static final long DEFAULT_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS =
+                DEFAULT_RUNTIME_MIN_GUARANTEE_MS;
+        public static final long DEFAULT_RUNTIME_DATA_TRANSFER_LIMIT_MS =
+                DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
+        public static final long DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS =
+                Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS);
+        public static final long DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS =
+                Math.max(60 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS);
+        public static final float
+                DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.35f;
+        public static final long DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS =
+                Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS);
+        public static final long DEFAULT_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS =
+                Math.max(Long.MAX_VALUE, DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS);
         static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = true;
         private static final boolean DEFAULT_USE_TARE_POLICY = false;
 
@@ -700,6 +738,49 @@
                 DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS;
 
         /**
+         * The minimum amount of time we try to guarantee normal data transfer jobs will run for.
+         */
+        public long RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS =
+                DEFAULT_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS;
+
+        /**
+         * The maximum amount of time we will let a normal data transfer job run for. This will only
+         * apply if there are no other limits that apply to the specific data transfer job.
+         */
+        public long RUNTIME_DATA_TRANSFER_LIMIT_MS = DEFAULT_RUNTIME_DATA_TRANSFER_LIMIT_MS;
+
+        /**
+         * The minimum amount of time we try to guarantee normal user-initiated jobs will run for.
+         */
+        public long RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS =
+                DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS;
+
+        /**
+         * The maximum amount of time we will let a user-initiated job run for. This will only
+         * apply if there are no other limits that apply to the specific user-initiated job.
+         */
+        public long RUNTIME_USER_INITIATED_LIMIT_MS = DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS;
+
+        /**
+         * A factor to apply to estimated transfer durations for user-initiated data transfer jobs
+         * so that we give some extra time for unexpected situations. This will be at least 1 and
+         * so can just be multiplied with the original value to get the final value.
+         */
+        public float RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR =
+                DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR;
+
+        /**
+         * The minimum amount of time we try to guarantee user-initiated data transfer jobs
+         * will run for.
+         */
+        public long RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS =
+                DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS;
+
+        /** The maximum amount of time we will let a user-initiated data transfer job run for. */
+        public long RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS =
+                DEFAULT_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS;
+
+        /**
          * Whether to persist jobs in split files (by UID). If false, all persisted jobs will be
          * saved in a single file.
          */
@@ -801,7 +882,14 @@
                     DeviceConfig.NAMESPACE_JOB_SCHEDULER,
                     KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                     KEY_RUNTIME_MIN_GUARANTEE_MS, KEY_RUNTIME_MIN_EJ_GUARANTEE_MS,
-                    KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS);
+                    KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS,
+                    KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR,
+                    KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+                    KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS,
+                    KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+                    KEY_RUNTIME_USER_INITIATED_LIMIT_MS,
+                    KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+                    KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS);
 
             // Make sure min runtime for regular jobs is at least 10 minutes.
             RUNTIME_MIN_GUARANTEE_MS = Math.max(10 * MINUTE_IN_MILLIS,
@@ -819,6 +907,49 @@
             RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS,
                     properties.getLong(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                             DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS));
+            // Make sure min runtime is at least as long as regular jobs.
+            RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS,
+                    properties.getLong(
+                            KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+                            DEFAULT_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS));
+            // Max limit should be at least the min guarantee AND the free quota.
+            RUNTIME_DATA_TRANSFER_LIMIT_MS = Math.max(RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+                    Math.max(RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+                            properties.getLong(
+                                    KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS,
+                                    DEFAULT_RUNTIME_DATA_TRANSFER_LIMIT_MS)));
+            // Make sure min runtime is at least as long as regular jobs.
+            RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS,
+                    properties.getLong(
+                            KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+                            DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS));
+            // Max limit should be at least the min guarantee AND the free quota.
+            RUNTIME_USER_INITIATED_LIMIT_MS = Math.max(RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+                    Math.max(RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+                            properties.getLong(
+                                    KEY_RUNTIME_USER_INITIATED_LIMIT_MS,
+                                    DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS)));
+            // The buffer factor should be at least 1 (so we don't decrease the time).
+            RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = Math.max(1,
+                    properties.getFloat(
+                            KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR,
+                            DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR
+                    ));
+            // Make sure min runtime is at least as long as other user-initiated jobs.
+            RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = Math.max(
+                    RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+                    properties.getLong(
+                            KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+                            DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS));
+            // Data transfer requires RUN_LONG_JOBS permission, so the upper limit will be higher
+            // than other jobs.
+            // Max limit should be the min guarantee and the max of other user-initiated jobs.
+            RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = Math.max(
+                    RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+                    Math.max(RUNTIME_USER_INITIATED_LIMIT_MS,
+                            properties.getLong(
+                                    KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS,
+                                    DEFAULT_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS)));
         }
 
         private boolean updateTareSettingsLocked(boolean isTareEnabled) {
@@ -867,6 +998,20 @@
                     RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS).println();
             pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)
                     .println();
+            pw.print(KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+                    RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS).println();
+            pw.print(KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS,
+                    RUNTIME_DATA_TRANSFER_LIMIT_MS).println();
+            pw.print(KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+                    RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS).println();
+            pw.print(KEY_RUNTIME_USER_INITIATED_LIMIT_MS,
+                    RUNTIME_USER_INITIATED_LIMIT_MS).println();
+            pw.print(KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR,
+                    RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR).println();
+            pw.print(KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+                    RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS).println();
+            pw.print(KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS,
+                    RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS).println();
 
             pw.print(KEY_PERSIST_IN_SPLIT_FILES, PERSIST_IN_SPLIT_FILES).println();
 
@@ -1858,9 +2003,9 @@
         final FlexibilityController flexibilityController =
                 new FlexibilityController(this, mPrefetchController);
         mControllers.add(flexibilityController);
-        final ConnectivityController connectivityController =
+        mConnectivityController =
                 new ConnectivityController(this, flexibilityController);
-        mControllers.add(connectivityController);
+        mControllers.add(mConnectivityController);
         mControllers.add(new TimeController(this));
         final IdleController idleController = new IdleController(this, flexibilityController);
         mControllers.add(idleController);
@@ -1876,16 +2021,16 @@
         mDeviceIdleJobsController = new DeviceIdleJobsController(this);
         mControllers.add(mDeviceIdleJobsController);
         mQuotaController =
-                new QuotaController(this, backgroundJobsController, connectivityController);
+                new QuotaController(this, backgroundJobsController, mConnectivityController);
         mControllers.add(mQuotaController);
         mControllers.add(new ComponentController(this));
         mTareController =
-                new TareController(this, backgroundJobsController, connectivityController);
+                new TareController(this, backgroundJobsController, mConnectivityController);
         mControllers.add(mTareController);
 
         mRestrictiveControllers = new ArrayList<>();
         mRestrictiveControllers.add(batteryController);
-        mRestrictiveControllers.add(connectivityController);
+        mRestrictiveControllers.add(mConnectivityController);
         mRestrictiveControllers.add(idleController);
 
         // Create restrictions
@@ -3028,7 +3173,30 @@
     /** Returns the minimum amount of time we should let this job run before timing out. */
     public long getMinJobExecutionGuaranteeMs(JobStatus job) {
         synchronized (mLock) {
-            if (job.shouldTreatAsExpeditedJob()) {
+            final boolean shouldTreatAsDataTransfer = job.getJob().isDataTransfer()
+                    && checkRunLongJobsPermission(job.getSourceUid(), job.getSourcePackageName());
+            if (job.shouldTreatAsUserInitiated()) {
+                if (shouldTreatAsDataTransfer) {
+                    final long estimatedTransferTimeMs =
+                            mConnectivityController.getEstimatedTransferTimeMs(job);
+                    if (estimatedTransferTimeMs == ConnectivityController.UNKNOWN_TIME) {
+                        return mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS;
+                    }
+                    // Try to give the job at least as much time as we think the transfer will take,
+                    // but cap it at the maximum limit
+                    final long factoredTransferTimeMs = (long) (estimatedTransferTimeMs
+                            * mConstants
+                            .RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR);
+                    return Math.min(mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS,
+                            Math.max(factoredTransferTimeMs,
+                                    mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS
+                            ));
+                }
+                return mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS;
+            } else if (shouldTreatAsDataTransfer) {
+                // For now, don't increase a bg data transfer's minimum guarantee.
+                return mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS;
+            } else if (job.shouldTreatAsExpeditedJob()) {
                 // Don't guarantee RESTRICTED jobs more than 5 minutes.
                 return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX
                         ? mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS
@@ -3044,6 +3212,16 @@
     /** Returns the maximum amount of time this job could run for. */
     public long getMaxJobExecutionTimeMs(JobStatus job) {
         synchronized (mLock) {
+            final boolean shouldTreatAsDataTransfer = job.getJob().isDataTransfer()
+                    && checkRunLongJobsPermission(job.getSourceUid(), job.getSourcePackageName());
+            if (job.shouldTreatAsUserInitiated()) {
+                if (shouldTreatAsDataTransfer) {
+                    return mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS;
+                }
+                return mConstants.RUNTIME_USER_INITIATED_LIMIT_MS;
+            } else if (shouldTreatAsDataTransfer) {
+                return mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS;
+            }
             return Math.min(mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                     mConstants.USE_TARE_POLICY
                             ? mTareController.getMaxJobExecutionTimeMsLocked(job)
@@ -3703,13 +3881,6 @@
             return checkRunLongJobsPermission(uid, packageName);
         }
 
-        private boolean checkRunLongJobsPermission(int packageUid, String packageName) {
-            // Returns true if both the appop and permission are granted.
-            return PermissionChecker.checkPermissionForPreflight(getContext(),
-                    android.Manifest.permission.RUN_LONG_JOBS, PermissionChecker.PID_UNKNOWN,
-                    packageUid, packageName) == PermissionChecker.PERMISSION_GRANTED;
-        }
-
         /**
          * "dumpsys" infrastructure
          */
@@ -3985,13 +4156,27 @@
         }
     }
 
+    private boolean checkRunLongJobsPermission(int packageUid, String packageName) {
+        // Returns true if both the appop and permission are granted.
+        return PermissionChecker.checkPermissionForPreflight(getTestableContext(),
+                android.Manifest.permission.RUN_LONG_JOBS, PermissionChecker.PID_UNKNOWN,
+                packageUid, packageName) == PermissionChecker.PERMISSION_GRANTED;
+    }
+
+    @VisibleForTesting
+    protected ConnectivityController getConnectivityController() {
+        return mConnectivityController;
+    }
+
     // Shell command infrastructure
     int getJobState(PrintWriter pw, String pkgName, int userId, int jobId) {
         try {
             final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
                     userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
             if (uid < 0) {
-                pw.print("unknown("); pw.print(pkgName); pw.println(")");
+                pw.print("unknown(");
+                pw.print(pkgName);
+                pw.println(")");
                 return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE;
             }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
index 16dd1672..6166921 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -42,7 +42,6 @@
 import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
-import android.util.DataUnit;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Pools;
@@ -82,6 +81,8 @@
     private static final boolean DEBUG = JobSchedulerService.DEBUG
             || Log.isLoggable(TAG, Log.DEBUG);
 
+    public static final long UNKNOWN_TIME = -1L;
+
     // The networking stack has a hard limit so we can't make this configurable.
     private static final int MAX_NETWORK_CALLBACKS = 125;
     /**
@@ -570,9 +571,8 @@
             // If we don't know the bandwidth, all we can do is hope the job finishes the minimum
             // chunk in time.
             if (bandwidthDown > 0) {
-                // Divide by 8 to convert bits to bytes.
-                final long estimatedMillis = ((minimumChunkBytes * DateUtils.SECOND_IN_MILLIS)
-                        / (DataUnit.KIBIBYTES.toBytes(bandwidthDown) / 8));
+                final long estimatedMillis =
+                        calculateTransferTimeMs(minimumChunkBytes, bandwidthDown);
                 if (estimatedMillis > maxJobExecutionTimeMs) {
                     // If we'd never finish the minimum chunk before the timeout, we'd be insane!
                     Slog.w(TAG, "Minimum chunk " + minimumChunkBytes + " bytes over "
@@ -585,9 +585,8 @@
             final long bandwidthUp = capabilities.getLinkUpstreamBandwidthKbps();
             // If we don't know the bandwidth, all we can do is hope the job finishes in time.
             if (bandwidthUp > 0) {
-                // Divide by 8 to convert bits to bytes.
-                final long estimatedMillis = ((minimumChunkBytes * DateUtils.SECOND_IN_MILLIS)
-                        / (DataUnit.KIBIBYTES.toBytes(bandwidthUp) / 8));
+                final long estimatedMillis =
+                        calculateTransferTimeMs(minimumChunkBytes, bandwidthUp);
                 if (estimatedMillis > maxJobExecutionTimeMs) {
                     // If we'd never finish the minimum chunk before the timeout, we'd be insane!
                     Slog.w(TAG, "Minimum chunk " + minimumChunkBytes + " bytes over " + bandwidthUp
@@ -615,9 +614,7 @@
             final long bandwidth = capabilities.getLinkDownstreamBandwidthKbps();
             // If we don't know the bandwidth, all we can do is hope the job finishes in time.
             if (bandwidth > 0) {
-                // Divide by 8 to convert bits to bytes.
-                final long estimatedMillis = ((downloadBytes * DateUtils.SECOND_IN_MILLIS)
-                        / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8));
+                final long estimatedMillis = calculateTransferTimeMs(downloadBytes, bandwidth);
                 if (estimatedMillis > maxJobExecutionTimeMs) {
                     // If we'd never finish before the timeout, we'd be insane!
                     Slog.w(TAG, "Estimated " + downloadBytes + " download bytes over " + bandwidth
@@ -633,9 +630,7 @@
             final long bandwidth = capabilities.getLinkUpstreamBandwidthKbps();
             // If we don't know the bandwidth, all we can do is hope the job finishes in time.
             if (bandwidth > 0) {
-                // Divide by 8 to convert bits to bytes.
-                final long estimatedMillis = ((uploadBytes * DateUtils.SECOND_IN_MILLIS)
-                        / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8));
+                final long estimatedMillis = calculateTransferTimeMs(uploadBytes, bandwidth);
                 if (estimatedMillis > maxJobExecutionTimeMs) {
                     // If we'd never finish before the timeout, we'd be insane!
                     Slog.w(TAG, "Estimated " + uploadBytes + " upload bytes over " + bandwidth
@@ -649,6 +644,48 @@
         return false;
     }
 
+    /**
+     * Return the estimated amount of time this job will be transferring data,
+     * based on the current network speed.
+     */
+    public long getEstimatedTransferTimeMs(JobStatus jobStatus) {
+        final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes();
+        final long uploadBytes = jobStatus.getEstimatedNetworkUploadBytes();
+        if (downloadBytes == JobInfo.NETWORK_BYTES_UNKNOWN
+                && uploadBytes == JobInfo.NETWORK_BYTES_UNKNOWN) {
+            return UNKNOWN_TIME;
+        }
+        if (jobStatus.network == null) {
+            // This job doesn't have a network assigned.
+            return UNKNOWN_TIME;
+        }
+        NetworkCapabilities capabilities = getNetworkCapabilities(jobStatus.network);
+        if (capabilities == null) {
+            return UNKNOWN_TIME;
+        }
+        final long estimatedDownloadTimeMs = calculateTransferTimeMs(downloadBytes,
+                capabilities.getLinkDownstreamBandwidthKbps());
+        final long estimatedUploadTimeMs = calculateTransferTimeMs(uploadBytes,
+                capabilities.getLinkUpstreamBandwidthKbps());
+        if (estimatedDownloadTimeMs == UNKNOWN_TIME) {
+            return estimatedUploadTimeMs;
+        } else if (estimatedUploadTimeMs == UNKNOWN_TIME) {
+            return estimatedDownloadTimeMs;
+        }
+        return estimatedDownloadTimeMs + estimatedUploadTimeMs;
+    }
+
+    @VisibleForTesting
+    static long calculateTransferTimeMs(long transferBytes, long bandwidthKbps) {
+        if (transferBytes == JobInfo.NETWORK_BYTES_UNKNOWN || bandwidthKbps <= 0) {
+            return UNKNOWN_TIME;
+        }
+        return (transferBytes * DateUtils.SECOND_IN_MILLIS)
+                // Multiply by 1000 to convert kilobits to bits.
+                // Divide by 8 to convert bits to bytes.
+                / (bandwidthKbps * 1000 / 8);
+    }
+
     private static boolean isCongestionDelayed(JobStatus jobStatus, Network network,
             NetworkCapabilities capabilities, Constants constants) {
         // If network is congested, and job is less than 50% through the
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 83b6a8e..419127e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -1345,6 +1345,15 @@
     }
 
     /**
+     * @return true if the job was scheduled as a user-initiated job and it hasn't been downgraded
+     * for any reason.
+     */
+    public boolean shouldTreatAsUserInitiated() {
+        // TODO(248386641): implement
+        return false;
+    }
+
+    /**
      * Return a summary that uniquely identifies the underlying job.
      */
     @NonNull
diff --git a/api/Android.bp b/api/Android.bp
index b0ce9af..318748e 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -118,9 +118,11 @@
     ],
     system_server_classpath: [
         "service-art",
+        "service-configinfrastructure",
         "service-healthconnect",
         "service-media-s",
         "service-permission",
+        "service-rkp",
         "service-sdksandbox",
     ],
 }
diff --git a/core/api/current.txt b/core/api/current.txt
index fb3fd0c..cefc856 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -7475,7 +7475,7 @@
     method public boolean getBluetoothContactSharingDisabled(@NonNull android.content.ComponentName);
     method public boolean getCameraDisabled(@Nullable android.content.ComponentName);
     method @Deprecated @Nullable public String getCertInstallerPackage(@NonNull android.content.ComponentName) throws java.lang.SecurityException;
-    method @Nullable public java.util.Set<java.lang.String> getCrossProfileCalendarPackages(@NonNull android.content.ComponentName);
+    method @Deprecated @Nullable public java.util.Set<java.lang.String> getCrossProfileCalendarPackages(@NonNull android.content.ComponentName);
     method public boolean getCrossProfileCallerIdDisabled(@NonNull android.content.ComponentName);
     method public boolean getCrossProfileContactsSearchDisabled(@NonNull android.content.ComponentName);
     method @NonNull public java.util.Set<java.lang.String> getCrossProfilePackages(@NonNull android.content.ComponentName);
@@ -7624,7 +7624,7 @@
     method @Deprecated public void setCertInstallerPackage(@NonNull android.content.ComponentName, @Nullable String) throws java.lang.SecurityException;
     method public void setCommonCriteriaModeEnabled(@NonNull android.content.ComponentName, boolean);
     method public void setConfiguredNetworksLockdownState(@NonNull android.content.ComponentName, boolean);
-    method public void setCrossProfileCalendarPackages(@NonNull android.content.ComponentName, @Nullable java.util.Set<java.lang.String>);
+    method @Deprecated public void setCrossProfileCalendarPackages(@NonNull android.content.ComponentName, @Nullable java.util.Set<java.lang.String>);
     method public void setCrossProfileCallerIdDisabled(@NonNull android.content.ComponentName, boolean);
     method public void setCrossProfileContactsSearchDisabled(@NonNull android.content.ComponentName, boolean);
     method public void setCrossProfilePackages(@NonNull android.content.ComponentName, @NonNull java.util.Set<java.lang.String>);
@@ -20207,7 +20207,7 @@
 
   public final class AltitudeConverter {
     ctor public AltitudeConverter();
-    method @WorkerThread public void addMslAltitude(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException;
+    method @WorkerThread public void addMslAltitudeToLocation(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException;
   }
 
 }
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 43d4530..3d30c0f 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -38,6 +38,7 @@
     field public static final String READ_CELL_BROADCASTS = "android.permission.READ_CELL_BROADCASTS";
     field public static final String READ_PRIVILEGED_PHONE_STATE = "android.permission.READ_PRIVILEGED_PHONE_STATE";
     field public static final String RECORD_BACKGROUND_AUDIO = "android.permission.RECORD_BACKGROUND_AUDIO";
+    field public static final String REMAP_MODIFIER_KEYS = "android.permission.REMAP_MODIFIER_KEYS";
     field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS";
     field public static final String REQUEST_UNIQUE_ID_ATTESTATION = "android.permission.REQUEST_UNIQUE_ID_ATTESTATION";
     field public static final String RESET_APP_ERRORS = "android.permission.RESET_APP_ERRORS";
@@ -1294,8 +1295,11 @@
 
   public final class InputManager {
     method public void addUniqueIdAssociation(@NonNull String, @NonNull String);
+    method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void clearAllModifierKeyRemappings();
     method @Nullable public String getCurrentKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier);
     method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptorsForInputDevice(@NonNull android.view.InputDevice);
+    method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> getModifierKeyRemapping();
+    method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int);
     method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void removeKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String);
     method public void removeUniqueIdAssociation(@NonNull String);
     method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void setCurrentKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String);
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 128e3de..738e2de 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -368,6 +368,20 @@
     },
 }
 
+// Build Rust bindings for remote provisioning. Needed by keystore2.
+aidl_interface {
+    name: "android.security.rkp_aidl",
+    unstable: true,
+    srcs: [
+        "android/security/rkp/*.aidl",
+    ],
+    backend: {
+        rust: {
+            enabled: true,
+        },
+    },
+}
+
 aidl_interface {
     name: "android.debug_aidl",
     unstable: true,
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index ecea1bb..0784405 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -14449,7 +14449,9 @@
      * @throws SecurityException if {@code admin} is not a profile owner
      *
      * @see #getCrossProfileCalendarPackages(ComponentName)
+     * @deprecated Use {@link #setCrossProfilePackages(ComponentName, Set)}.
      */
+    @Deprecated
     public void setCrossProfileCalendarPackages(@NonNull ComponentName admin,
             @Nullable Set<String> packageNames) {
         throwIfParentInstance("setCrossProfileCalendarPackages");
@@ -14475,7 +14477,9 @@
      * @throws SecurityException if {@code admin} is not a profile owner
      *
      * @see #setCrossProfileCalendarPackages(ComponentName, Set)
+     * @deprecated Use {@link #setCrossProfilePackages(ComponentName, Set)}.
      */
+    @Deprecated
     public @Nullable Set<String> getCrossProfileCalendarPackages(@NonNull ComponentName admin) {
         throwIfParentInstance("getCrossProfileCalendarPackages");
         if (mService != null) {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 9f9fd3c..df5a1ed 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5937,6 +5937,14 @@
     public static final String FILE_INTEGRITY_SERVICE = "file_integrity";
 
     /**
+     * Binder service for remote key provisioning.
+     *
+     * @see android.frameworks.rkp.IRemoteProvisioning
+     * @hide
+     */
+    public static final String REMOTE_PROVISIONING_SERVICE = "remote_provisioning";
+
+    /**
      * Use with {@link #getSystemService(String)} to retrieve a
      * {@link android.hardware.lights.LightsManager} for controlling device lights.
      *
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index b26c0a2..eef0f42 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -123,6 +123,22 @@
     String[] getKeyboardLayoutListForInputDevice(in InputDeviceIdentifier identifier, int userId,
             in InputMethodInfo imeInfo, in InputMethodSubtype imeSubtype);
 
+    // Modifier key remapping APIs.
+    @EnforcePermission("REMAP_MODIFIER_KEYS")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+            + "android.Manifest.permission.REMAP_MODIFIER_KEYS)")
+    void remapModifierKey(int fromKey, int toKey);
+
+    @EnforcePermission("REMAP_MODIFIER_KEYS")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+            + "android.Manifest.permission.REMAP_MODIFIER_KEYS)")
+    void clearAllModifierKeyRemappings();
+
+    @EnforcePermission("REMAP_MODIFIER_KEYS")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+            + "android.Manifest.permission.REMAP_MODIFIER_KEYS)")
+    Map getModifierKeyRemapping();
+
     // Registers an input devices changed listener.
     void registerInputDevicesChangedListener(IInputDevicesChangedListener listener);
 
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index cea3fa1..3735417 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -78,6 +78,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -253,6 +254,31 @@
     })
     public @interface SwitchState {}
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "REMAPPABLE_MODIFIER_KEY_" }, value = {
+            RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_CTRL_LEFT,
+            RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_CTRL_RIGHT,
+            RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_META_LEFT,
+            RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_META_RIGHT,
+            RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_ALT_LEFT,
+            RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_ALT_RIGHT,
+            RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_SHIFT_LEFT,
+            RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_SHIFT_RIGHT,
+            RemappableModifierKey.REMAPPABLE_MODIFIER_KEY_CAPS_LOCK,
+    })
+    public @interface RemappableModifierKey {
+        int REMAPPABLE_MODIFIER_KEY_CTRL_LEFT = KeyEvent.KEYCODE_CTRL_LEFT;
+        int REMAPPABLE_MODIFIER_KEY_CTRL_RIGHT = KeyEvent.KEYCODE_CTRL_RIGHT;
+        int REMAPPABLE_MODIFIER_KEY_META_LEFT = KeyEvent.KEYCODE_META_LEFT;
+        int REMAPPABLE_MODIFIER_KEY_META_RIGHT = KeyEvent.KEYCODE_META_RIGHT;
+        int REMAPPABLE_MODIFIER_KEY_ALT_LEFT = KeyEvent.KEYCODE_ALT_LEFT;
+        int REMAPPABLE_MODIFIER_KEY_ALT_RIGHT = KeyEvent.KEYCODE_ALT_RIGHT;
+        int REMAPPABLE_MODIFIER_KEY_SHIFT_LEFT = KeyEvent.KEYCODE_SHIFT_LEFT;
+        int REMAPPABLE_MODIFIER_KEY_SHIFT_RIGHT = KeyEvent.KEYCODE_SHIFT_RIGHT;
+        int REMAPPABLE_MODIFIER_KEY_CAPS_LOCK = KeyEvent.KEYCODE_CAPS_LOCK;
+    }
+
     /**
      * Switch State: Unknown.
      *
@@ -854,6 +880,60 @@
     }
 
     /**
+     * Remaps modifier keys. Remapping a modifier key to itself will clear any previous remappings
+     * for that key.
+     *
+     * @param fromKey The modifier key getting remapped.
+     * @param toKey The modifier key that it is remapped to.
+     *
+     * @hide
+     */
+    @TestApi
+    @RequiresPermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+    public void remapModifierKey(@RemappableModifierKey int fromKey,
+            @RemappableModifierKey int toKey) {
+        try {
+            mIm.remapModifierKey(fromKey, toKey);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Clears all existing modifier key remappings
+     *
+     * @hide
+     */
+    @TestApi
+    @RequiresPermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+    public void clearAllModifierKeyRemappings() {
+        try {
+            mIm.clearAllModifierKeyRemappings();
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Provides the current modifier key remapping
+     *
+     * @return a {fromKey, toKey} map that contains the existing modifier key remappings..
+     * {@link RemappableModifierKey}
+     *
+     * @hide
+     */
+    @TestApi
+    @NonNull
+    @RequiresPermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+    public Map<Integer, Integer> getModifierKeyRemapping() {
+        try {
+            return mIm.getModifierKeyRemapping();
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Gets the TouchCalibration applied to the specified input device's coordinates.
      *
      * @param inputDeviceDescriptor The input device descriptor.
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index 5c5af2a..e9a3254 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -71,6 +71,9 @@
 # Tracing
 per-file Trace.java = file:/TRACE_OWNERS
 
+# PatternMatcher
+per-file PatternMatcher* = file:/PACKAGE_MANAGER_OWNERS
+
 # PermissionEnforcer
 per-file PermissionEnforcer.java = tweek@google.com, brufino@google.com
 
diff --git a/core/java/android/security/rkp/IGetKeyCallback.aidl b/core/java/android/security/rkp/IGetKeyCallback.aidl
new file mode 100644
index 0000000..85ceae62
--- /dev/null
+++ b/core/java/android/security/rkp/IGetKeyCallback.aidl
@@ -0,0 +1,49 @@
+/*
+ * 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 android.security.rkp;
+
+import android.security.rkp.RemotelyProvisionedKey;
+
+/**
+ * Callback interface for receiving remotely provisioned keys from a
+ * {@link IRegistration}.
+ *
+ * @hide
+ */
+oneway interface IGetKeyCallback {
+    /**
+     * Called in response to {@link IRegistration.getKey}, indicating
+     * a remotely-provisioned key is available.
+     *
+     * @param key The key that was received from the remote provisioning service.
+     */
+    void onSuccess(in RemotelyProvisionedKey key);
+
+    /**
+     * Called when the key request has been successfully cancelled.
+     * @see IRegistration.cancelGetKey
+     */
+    void onCancel();
+
+    /**
+     * Called when an error has occurred while trying to get a remotely provisioned key.
+     *
+     * @param error A description of what failed, suitable for logging.
+     */
+    void onError(String error);
+}
+
diff --git a/core/java/android/security/rkp/IGetRegistrationCallback.aidl b/core/java/android/security/rkp/IGetRegistrationCallback.aidl
new file mode 100644
index 0000000..e375a6f
--- /dev/null
+++ b/core/java/android/security/rkp/IGetRegistrationCallback.aidl
@@ -0,0 +1,49 @@
+/*
+ * 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 android.security.rkp;
+
+import android.security.rkp.IRegistration;
+
+/**
+ * Callback interface for receiving a remote provisioning registration.
+ * {@link IRegistration}.
+ *
+ * @hide
+ */
+oneway interface IGetRegistrationCallback {
+    /**
+     * Called in response to {@link IRemoteProvisioning.getRegistration}.
+     *
+     * @param registration an IRegistration that is used to fetch remotely
+     * provisioned keys for the given IRemotelyProvisionedComponent.
+     */
+    void onSuccess(in IRegistration registration);
+
+    /**
+     * Called when the get registration request has been successfully cancelled.
+     * @see IRemoteProvisioning.cancelGetRegistration
+     */
+    void onCancel();
+
+    /**
+     * Called when an error has occurred while trying to get a registration.
+     *
+     * @param error A description of what failed, suitable for logging.
+     */
+    void onError(String error);
+}
+
diff --git a/core/java/android/security/rkp/IRegistration.aidl b/core/java/android/security/rkp/IRegistration.aidl
new file mode 100644
index 0000000..6522a45
--- /dev/null
+++ b/core/java/android/security/rkp/IRegistration.aidl
@@ -0,0 +1,85 @@
+/*
+ * 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 android.security.rkp;
+
+import android.security.rkp.IGetKeyCallback;
+
+/**
+ * This interface is associated with the registration of an
+ * IRemotelyProvisionedComponent. Each component has a unique set of keys
+ * and certificates that are provisioned to the device for attestation. An
+ * IRegistration binder is created by calling
+ * {@link IRemoteProvisioning#getRegistration()}.
+ *
+ * This interface is used to query for available keys and certificates for the
+ * registered component.
+ *
+ * @hide
+ */
+oneway interface IRegistration {
+    /**
+     * Fetch a remotely provisioned key for the given keyId. Keys are unique
+     * per caller/keyId/registration tuple. This ensures that no two
+     * applications are able to correlate keys to uniquely identify a
+     * device/user. Callers receive their key via {@code callback}.
+     *
+     * If a key is available, this call immediately invokes {@code callback}.
+     *
+     * If no keys are immediately available, then this function contacts the
+     * remote provisioning server to provision a key. After provisioning is
+     * completed, the key is passed to {@code callback}.
+     *
+     * @param keyId This is a client-chosen key identifier, used to
+     * differentiate between keys for varying client-specific use-cases. For
+     * example, keystore2 passes the UID of the applications that call it as
+     * the keyId value here, so that each of keystore2's clients gets a unique
+     * key.
+     * @param callback Receives the result of the call. A callback must only
+     * be used with one {@code getKey} call at a time.
+     */
+    void getKey(int keyId, IGetKeyCallback callback);
+
+    /**
+     * Cancel an active request for a remotely provisioned key, as initiated via
+     * {@link getKey}. Upon cancellation, {@code callback.onCancel} will be invoked.
+     */
+    void cancelGetKey(IGetKeyCallback callback);
+
+    /**
+     * Replace an obsolete key blob with an upgraded key blob.
+     * In certain cases, such as security patch level upgrade, keys become "old".
+     * In these cases, the component which supports operations with the remotely
+     * provisioned key blobs must support upgrading the blobs to make them "new"
+     * and usable on the updated system.
+     *
+     * For an example of a remotely provisioned component that has an upgrade
+     * mechanism, see the documentation for IKeyMintDevice.upgradeKey.
+     *
+     * Once a key has been upgraded, the IRegistration where the key is stored
+     * needs to be told about the new blob. After calling storeUpgradedKey,
+     * getKey will return the new key blob instead of the old one.
+     *
+     * Note that this function does NOT extend the lifetime of key blobs. The
+     * certificate for the key is unchanged, and the key will still expire at
+     * the same time it would have if storeUpgradedKey had never been called.
+     *
+     * @param oldKeyBlob The old key blob to be replaced by {@code newKeyBlob}.
+     *
+     * @param newKeyblob The new blob to replace {@code oldKeyBlob}.
+     */
+    void storeUpgradedKey(in byte[] oldKeyBlob, in byte[] newKeyBlob);
+}
diff --git a/core/java/android/security/rkp/IRemoteProvisioning.aidl b/core/java/android/security/rkp/IRemoteProvisioning.aidl
new file mode 100644
index 0000000..23d8159
--- /dev/null
+++ b/core/java/android/security/rkp/IRemoteProvisioning.aidl
@@ -0,0 +1,68 @@
+/*
+ * 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 android.security.rkp;
+
+import android.security.rkp.IRegistration;
+import android.security.rkp.IGetRegistrationCallback;
+
+/**
+ * {@link IRemoteProvisioning} is the interface provided to use the remote key
+ * provisioning functionality from the Remote Key Provisioning Daemon (RKPD).
+ * This would be the first service that RKPD clients would interact with. The
+ * intent is for the clients to get the {@link IRegistration} object from this
+ * interface and use it for actual remote provisioning work.
+ *
+ * @hide
+ */
+oneway interface IRemoteProvisioning {
+    /**
+     * Takes a remotely provisioned component service name and gets a
+     * registration bound to that service and the caller's UID.
+     *
+     * @param irpcName The name of the {@code IRemotelyProvisionedComponent}
+     * for which remotely provisioned keys should be managed.
+     * @param callback Receives the result of the call. A callback must only
+     * be used with one {@code getRegistration} call at a time.
+     *
+     * Notes:
+     * - This function will attempt to get the service named by irpcName. This
+     *   implies that a lazy/dynamic aidl service will be instantiated, and this
+     *   function blocks until the service is up. Upon return, any binder tokens
+     *   are dropped, allowing the lazy/dynamic service to shutdown.
+     * - The created registration object is unique per caller. If two different
+     *   UIDs call getRegistration with the same irpcName, they will receive
+     *   different registrations. This prevents two different applications from
+     *   being able to see the same keys.
+     * - This function is idempotent per calling UID. Additional calls to
+     *   getRegistration with the same parameters, from the same caller, will have
+     *   no side effects.
+     * - A callback may only be associated with one getRegistration call at a time.
+     *   If the callback is used multiple times, this API will return an error.
+     *
+     * @see IRegistration#getKey()
+     * @see IRemotelyProvisionedComponent
+     *
+     */
+    void getRegistration(String irpcName, IGetRegistrationCallback callback);
+
+    /**
+     * Cancel any active {@link getRegistration} call associated with the given
+     * callback. If no getRegistration call is currently active, this function is
+     * a noop.
+     */
+    void cancelGetRegistration(IGetRegistrationCallback callback);
+}
diff --git a/core/java/android/security/rkp/OWNERS b/core/java/android/security/rkp/OWNERS
new file mode 100644
index 0000000..fd43089
--- /dev/null
+++ b/core/java/android/security/rkp/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 1084908
+
+jbires@google.com
+sethmo@google.com
+vikramgaur@google.com
diff --git a/core/java/android/security/rkp/RemotelyProvisionedKey.aidl b/core/java/android/security/rkp/RemotelyProvisionedKey.aidl
new file mode 100644
index 0000000..207f18f
--- /dev/null
+++ b/core/java/android/security/rkp/RemotelyProvisionedKey.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright 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 android.security.rkp;
+
+/**
+ * A {@link RemotelyProvisionedKey} holds an attestation key and the
+ * corresponding remotely provisioned certificate chain.
+ *
+ * @hide
+ */
+@RustDerive(Eq=true, PartialEq=true)
+parcelable RemotelyProvisionedKey {
+    /**
+     * The remotely-provisioned key that may be used to sign attestations. The
+     * format of this key is opaque, and need only be understood by the
+     * IRemotelyProvisionedComponent that generated it.
+     *
+     * Any private key material contained within this blob must be encrypted.
+     *
+     * @see IRemotelyProvisionedComponent
+     */
+    byte[] keyBlob;
+
+    /**
+     * Sequence of DER-encoded X.509 certificates that make up the attestation
+     * key's certificate chain. This is the binary encoding for a chain that is
+     * supported by Java's CertificateFactory.generateCertificates API.
+     */
+    byte[] encodedCertChain;
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 71a02dd..dc24b0f9 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6776,6 +6776,14 @@
          @hide -->
     <permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART"
                 android:protectionLevel="signature" />
+
+    <!-- Allows low-level access to re-mapping modifier keys.
+         <p>Not for use by third-party applications.
+         @hide
+         @TestApi -->
+    <permission android:name="android.permission.REMAP_MODIFIER_KEYS"
+                android:protectionLevel="signature" />
+
     <uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" />
 
     <!-- Allows financed device kiosk apps to perform actions on the Device Lock service
diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java
index 506128e..d46b4d2 100644
--- a/location/java/android/location/altitude/AltitudeConverter.java
+++ b/location/java/android/location/altitude/AltitudeConverter.java
@@ -151,20 +151,17 @@
      * altitude accuracy if the {@code location} has a finite and non-negative vertical accuracy;
      * otherwise, does not add a corresponding accuracy.
      *
-     * <p>Must be called off the main thread as data may be loaded from raw assets. Throws an
-     * {@link IOException} if an I/O error occurs when loading data.
+     * <p>Must be called off the main thread as data may be loaded from raw assets.
      *
-     * <p>Throws an {@link IllegalArgumentException} if the {@code location} has an invalid
-     * latitude, longitude, or altitude above WGS84. Specifically:
-     *
-     * <ul>
-     *     <li>The latitude must be between -90 and 90, both inclusive.
-     *     <li>The longitude must be between -180 and 180, both inclusive.
-     *     <li>The altitude above WGS84 must be finite.
-     * </ul>
+     * @throws IOException              if an I/O error occurs when loading data from raw assets.
+     * @throws IllegalArgumentException if the {@code location} has an invalid latitude, longitude,
+     *                                  or altitude above WGS84. Specifically, the latitude must be
+     *                                  between -90 and 90 (both inclusive), the longitude must be
+     *                                  between -180 and 180 (both inclusive), and the altitude
+     *                                  above WGS84 must be finite.
      */
     @WorkerThread
-    public void addMslAltitude(@NonNull Context context, @NonNull Location location)
+    public void addMslAltitudeToLocation(@NonNull Context context, @NonNull Location location)
             throws IOException {
         validate(location);
         MapParamsProto params = GeoidHeightMap.getParams(context);
diff --git a/media/tests/AudioPolicyTest/AndroidManifest.xml b/media/tests/AudioPolicyTest/AndroidManifest.xml
index f696735..5c911b1 100644
--- a/media/tests/AudioPolicyTest/AndroidManifest.xml
+++ b/media/tests/AudioPolicyTest/AndroidManifest.xml
@@ -24,13 +24,22 @@
 
     <application>
         <uses-library android:name="android.test.runner" />
-        <activity android:label="@string/app_name" android:name="AudioPolicyTestActivity"
+        <activity android:label="@string/app_name" android:name="AudioVolumeTestActivity"
                   android:screenOrientation="landscape" android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+        <activity android:label="@string/app_name" android:name="AudioPolicyDeathTestActivity"
+                  android:screenOrientation="landscape"
+                  android:process=":AudioPolicyDeathTestActivityProcess"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java
new file mode 100644
index 0000000..841804b
--- /dev/null
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.audiopolicytest;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioPolicyDeathTest {
+    private static final String TAG = "AudioPolicyDeathTest";
+
+    private static final int SAMPLE_RATE = 48000;
+    private static final int PLAYBACK_TIME_MS = 2000;
+
+    private static final IntentFilter AUDIO_NOISY_INTENT_FILTER =
+            new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
+
+    private class MyBroadcastReceiver extends BroadcastReceiver {
+        private boolean mReceived = false;
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
+                synchronized (this) {
+                    mReceived = true;
+                    notify();
+                }
+            }
+        }
+
+        public synchronized boolean received() {
+            return mReceived;
+        }
+    }
+    private final MyBroadcastReceiver mReceiver = new MyBroadcastReceiver();
+
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = getApplicationContext();
+        assertEquals(PackageManager.PERMISSION_GRANTED,
+                mContext.checkSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING));
+    }
+
+    //-----------------------------------------------------------------
+    // Tests that an AUDIO_BECOMING_NOISY intent is broadcast when an app having registered
+    // a dynamic audio policy that intercepts an active media playback dies
+    //-----------------------------------------------------------------
+    @Test
+    public void testPolicyClientDeathSendBecomingNoisyIntent() {
+        mContext.registerReceiver(mReceiver, AUDIO_NOISY_INTENT_FILTER);
+
+        // Launch process registering a dynamic auido policy and dying after PLAYBACK_TIME_MS/2 ms
+        Intent intent = new Intent(mContext, AudioPolicyDeathTestActivity.class);
+        intent.putExtra("captureDurationMs", PLAYBACK_TIME_MS / 2);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        mContext.startActivity(intent);
+
+        AudioTrack track = createAudioTrack();
+        track.play();
+        synchronized (mReceiver) {
+            long startTimeMs = System.currentTimeMillis();
+            long elapsedTimeMs = 0;
+            while (elapsedTimeMs < PLAYBACK_TIME_MS && !mReceiver.received()) {
+                try {
+                    mReceiver.wait(PLAYBACK_TIME_MS - elapsedTimeMs);
+                } catch (InterruptedException e) {
+                    Log.w(TAG, "wait interrupted");
+                }
+                elapsedTimeMs = System.currentTimeMillis() - startTimeMs;
+            }
+        }
+
+        track.stop();
+        track.release();
+
+        assertTrue(mReceiver.received());
+    }
+
+    private AudioTrack createAudioTrack() {
+        AudioFormat format = new AudioFormat.Builder()
+                .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
+                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+                .setSampleRate(SAMPLE_RATE)
+                .build();
+
+        short[] data = new short[PLAYBACK_TIME_MS * SAMPLE_RATE * format.getChannelCount() / 1000];
+        AudioAttributes attributes =
+                new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+
+        AudioTrack track = new AudioTrack(attributes, format, data.length,
+                AudioTrack.MODE_STATIC, AudioManager.AUDIO_SESSION_ID_GENERATE);
+        track.write(data, 0, data.length);
+
+        return track;
+    }
+}
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java
new file mode 100644
index 0000000..957e719
--- /dev/null
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java
@@ -0,0 +1,132 @@
+/*
+ * 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.audiopolicytest;
+
+import android.app.Activity;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
+import android.media.audiopolicy.AudioPolicy;
+import android.os.Bundle;
+import android.os.Looper;
+import android.util.Log;
+
+// This activity will register a dynamic audio policy to intercept media playback and launch
+// a thread that will capture audio from the policy mix and crash after the time indicated by
+// intent extra "captureDurationMs" has elapsed
+public class AudioPolicyDeathTestActivity extends Activity  {
+    private static final String TAG = "AudioPolicyDeathTestActivity";
+
+    private static final int SAMPLE_RATE = 48000;
+    private static final int RECORD_TIME_MS = 1000;
+
+    private AudioManager mAudioManager = null;
+    private AudioPolicy mAudioPolicy = null;
+
+    public AudioPolicyDeathTestActivity() {
+    }
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        mAudioManager = getApplicationContext().getSystemService(AudioManager.class);
+
+        AudioAttributes attributes = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_MEDIA).build();
+        AudioMixingRule.Builder audioMixingRuleBuilder = new AudioMixingRule.Builder()
+                .addRule(attributes, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
+
+        AudioFormat audioFormat = new AudioFormat.Builder()
+                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+                .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
+                .setSampleRate(SAMPLE_RATE)
+                .build();
+
+        AudioMix audioMix = new AudioMix.Builder(audioMixingRuleBuilder.build())
+                .setFormat(audioFormat)
+                .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
+                .build();
+
+        AudioPolicy.Builder audioPolicyBuilder = new AudioPolicy.Builder(getApplicationContext());
+        audioPolicyBuilder.addMix(audioMix)
+                .setLooper(Looper.getMainLooper());
+        mAudioPolicy = audioPolicyBuilder.build();
+
+        int result = mAudioManager.registerAudioPolicy(mAudioPolicy);
+        if (result != AudioManager.SUCCESS) {
+            Log.w(TAG, "registerAudioPolicy failed, status: " + result);
+            return;
+        }
+        AudioRecord audioRecord = mAudioPolicy.createAudioRecordSink(audioMix);
+        if (audioRecord == null) {
+            Log.w(TAG, "AudioRecord creation failed");
+            return;
+        }
+
+        int captureDurationMs = getIntent().getIntExtra("captureDurationMs", RECORD_TIME_MS);
+        AudioCapturingThread thread = new AudioCapturingThread(audioRecord, captureDurationMs);
+        thread.start();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mAudioManager != null && mAudioPolicy != null) {
+            mAudioManager.unregisterAudioPolicy(mAudioPolicy);
+        }
+    }
+
+    // A thread that captures audio from the supplied AudioRecord and crashes after the supplied
+    // duration has elapsed
+    private static class AudioCapturingThread extends Thread {
+        private final AudioRecord mAudioRecord;
+        private final int mDurationMs;
+
+        AudioCapturingThread(AudioRecord record, int durationMs) {
+            super();
+            mAudioRecord = record;
+            mDurationMs = durationMs;
+        }
+
+        @Override
+        @SuppressWarnings("ConstantOverflow")
+        public void run() {
+            int samplesLeft = mDurationMs * SAMPLE_RATE * mAudioRecord.getChannelCount() / 1000;
+            short[] readBuffer = new short[samplesLeft / 10];
+            mAudioRecord.startRecording();
+            long startTimeMs = System.currentTimeMillis();
+            long elapsedTimeMs = 0;
+            do {
+                int read = readBuffer.length < samplesLeft ? readBuffer.length : samplesLeft;
+                read = mAudioRecord.read(readBuffer, 0, read);
+                elapsedTimeMs = System.currentTimeMillis() - startTimeMs;
+                if (read < 0) {
+                    Log.w(TAG, "read error: " + read);
+                    break;
+                }
+                samplesLeft -= read;
+            } while (elapsedTimeMs < mDurationMs && samplesLeft > 0);
+
+            // force process to crash
+            int i = 1 / 0;
+        }
+    }
+}
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestActivity.java
similarity index 91%
rename from media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java
rename to media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestActivity.java
index e31c01a..8f61815 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyTestActivity.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeTestActivity.java
@@ -19,9 +19,9 @@
 import android.app.Activity;
 import android.os.Bundle;
 
-public class AudioPolicyTestActivity extends Activity  {
+public class AudioVolumeTestActivity extends Activity  {
 
-    public AudioPolicyTestActivity() {
+    public AudioVolumeTestActivity() {
     }
 
     /** Called when the activity is first created. */
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
index fc3b198..c6ec7a6 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumesTestRule.java
@@ -100,7 +100,7 @@
 
     @Before
     public void setUp() throws Exception {
-        ActivityScenario.launch(AudioPolicyTestActivity.class);
+        ActivityScenario.launch(AudioVolumeTestActivity.class);
 
         mContext = getApplicationContext();
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
diff --git a/packages/SettingsLib/Spa/screenshot/Android.bp b/packages/SettingsLib/Spa/screenshot/Android.bp
new file mode 100644
index 0000000..4e6b646
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/Android.bp
@@ -0,0 +1,41 @@
+//
+// 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 {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "SpaScreenshotTests",
+    test_suites: ["device-tests"],
+
+    asset_dirs: ["assets"],
+
+    srcs: ["src/**/*.kt"],
+
+    certificate: "platform",
+
+    static_libs: [
+        "SpaLib",
+        "SpaLibTestUtils",
+        "androidx.compose.runtime_runtime",
+        "androidx.test.ext.junit",
+        "androidx.test.runner",
+        "mockito-target-minus-junit4",
+        "platform-screenshot-diff-core",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Spa/screenshot/AndroidManifest.xml b/packages/SettingsLib/Spa/screenshot/AndroidManifest.xml
new file mode 100644
index 0000000..d59a154
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.spa.screenshot">
+
+    <uses-sdk android:minSdkVersion="21"/>
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity android:name=".DebugActivity" android:exported="true" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:label="Screenshot tests for SpaLib"
+        android:targetPackage="com.android.settingslib.spa.screenshot">
+    </instrumentation>
+</manifest>
diff --git a/packages/SettingsLib/Spa/screenshot/AndroidTest.xml b/packages/SettingsLib/Spa/screenshot/AndroidTest.xml
new file mode 100644
index 0000000..e0c08e8
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/AndroidTest.xml
@@ -0,0 +1,36 @@
+<!--
+  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.
+  -->
+
+<configuration description="Runs screendiff tests.">
+    <option name="test-suite-tag" value="apct-instrumentation" />
+    <option name="test-suite-tag" value="apct" />
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <option name="optimized-property-setting" value="true" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="SpaScreenshotTests.apk" />
+    </target_preparer>
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys"
+            value="/data/user/0/com.android.settingslib.spa.screenshot/files/settings_screenshots" />
+        <option name="collect-on-run-ended-only" value="true" />
+    </metrics_collector>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="com.android.settingslib.spa.screenshot" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/dark_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_landscape_preference.png
new file mode 100644
index 0000000..6086e2d
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_landscape_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/dark_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_portrait_preference.png
new file mode 100644
index 0000000..aa6c5b7
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/phone/dark_portrait_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/light_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/light_landscape_preference.png
new file mode 100644
index 0000000..cac990c
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/phone/light_landscape_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/phone/light_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/phone/light_portrait_preference.png
new file mode 100644
index 0000000..f6298c0
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/phone/light_portrait_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_landscape_preference.png
new file mode 100644
index 0000000..9391eeb
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_landscape_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_portrait_preference.png
new file mode 100644
index 0000000..94e2843
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/tablet/dark_portrait_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/light_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_landscape_preference.png
new file mode 100644
index 0000000..b1d03c3
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_landscape_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/assets/tablet/light_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_portrait_preference.png
new file mode 100644
index 0000000..95f19da
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/assets/tablet/light_portrait_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt
new file mode 100644
index 0000000..814d4a1
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.settingslib.spa.screenshot
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.os.Build
+import android.view.View
+import platform.test.screenshot.matchers.MSSIMMatcher
+import platform.test.screenshot.matchers.PixelPerfectMatcher
+
+/** Draw this [View] into a [Bitmap]. */
+// TODO(b/195673633): Remove this once Compose screenshot tests use hardware rendering for their
+// tests.
+fun View.drawIntoBitmap(): Bitmap {
+    val bitmap =
+        Bitmap.createBitmap(
+            measuredWidth,
+            measuredHeight,
+            Bitmap.Config.ARGB_8888,
+        )
+    val canvas = Canvas(bitmap)
+    draw(canvas)
+    return bitmap
+}
+
+/**
+ * The [BitmapMatcher][platform.test.screenshot.matchers.BitmapMatcher] that should be used for
+ * screenshot *unit* tests.
+ */
+val UnitTestBitmapMatcher =
+    if (Build.CPU_ABI == "x86_64") {
+        // Different CPU architectures can sometimes end up rendering differently, so we can't do
+        // pixel-perfect matching on different architectures using the same golden. Given that our
+        // presubmits are run on cf_x86_64_phone, our goldens should be perfectly matched on the
+        // x86_64 architecture and use the Structural Similarity Index on others.
+        // TODO(b/237511747): Run our screenshot presubmit tests on arm64 instead so that we can
+        // do pixel perfect matching both at presubmit time and at development time with actual
+        // devices.
+        PixelPerfectMatcher()
+    } else {
+        MSSIMMatcher()
+    }
+
+/**
+ * The [BitmapMatcher][platform.test.screenshot.matchers.BitmapMatcher] that should be used for
+ * screenshot *unit* tests.
+ *
+ * We use the Structural Similarity Index for integration tests because they usually contain
+ * additional information and noise that shouldn't break the test.
+ */
+val IntegrationTestBitmapMatcher = MSSIMMatcher()
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt
new file mode 100644
index 0000000..d7f42b3
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.settingslib.spa.screenshot
+
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.DisplaySpec
+
+/**
+ * The emulations specs for all 8 permutations of:
+ * - phone or tablet.
+ * - dark of light mode.
+ * - portrait or landscape.
+ */
+val DeviceEmulationSpec.Companion.PhoneAndTabletFull
+    get() = PhoneAndTabletFullSpec
+
+private val PhoneAndTabletFullSpec =
+    DeviceEmulationSpec.forDisplays(Displays.Phone, Displays.Tablet)
+
+/**
+ * The emulations specs of:
+ * - phone + light mode + portrait.
+ * - phone + light mode + landscape.
+ * - tablet + dark mode + portrait.
+ *
+ * This allows to test the most important permutations of a screen/layout with only 3
+ * configurations.
+ */
+val DeviceEmulationSpec.Companion.PhoneAndTabletMinimal
+    get() = PhoneAndTabletMinimalSpec
+
+private val PhoneAndTabletMinimalSpec =
+    DeviceEmulationSpec.forDisplays(Displays.Phone, isDarkTheme = false) +
+        DeviceEmulationSpec.forDisplays(Displays.Tablet, isDarkTheme = true, isLandscape = false)
+
+object Displays {
+    val Phone =
+        DisplaySpec(
+            "phone",
+            width = 1440,
+            height = 3120,
+            densityDpi = 560,
+        )
+
+    val Tablet =
+        DisplaySpec(
+            "tablet",
+            width = 2560,
+            height = 1600,
+            densityDpi = 320,
+        )
+}
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt
new file mode 100644
index 0000000..25bc098
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.settingslib.spa.screenshot
+
+import androidx.test.platform.app.InstrumentationRegistry
+import platform.test.screenshot.GoldenImagePathManager
+import platform.test.screenshot.PathConfig
+
+/** A [GoldenImagePathManager] that should be used for all Settings screenshot tests. */
+class SettingsGoldenImagePathManager(
+    pathConfig: PathConfig,
+    assetsPathRelativeToBuildRoot: String
+) :
+    GoldenImagePathManager(
+        appContext = InstrumentationRegistry.getInstrumentation().context,
+        assetsPathRelativeToBuildRoot = assetsPathRelativeToBuildRoot,
+        deviceLocalPath =
+        InstrumentationRegistry.getInstrumentation()
+            .targetContext
+            .filesDir
+            .absolutePath
+            .toString() + "/settings_screenshots",
+        pathConfig = pathConfig,
+    ) {
+    override fun toString(): String {
+        // This string is appended to all actual/expected screenshots on the device, so make sure
+        // it is a static value.
+        return "SettingsGoldenImagePathManager"
+    }
+}
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt
new file mode 100644
index 0000000..7a7cf31
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.settingslib.spa.screenshot
+
+import androidx.activity.ComponentActivity
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onRoot
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import platform.test.screenshot.DeviceEmulationRule
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.MaterialYouColorsRule
+import platform.test.screenshot.ScreenshotTestRule
+import platform.test.screenshot.getEmulatedDevicePathConfig
+
+/** A rule for Settings screenshot diff tests. */
+class SettingsScreenshotTestRule(
+    emulationSpec: DeviceEmulationSpec,
+    assetsPathRelativeToBuildRoot: String
+) : TestRule {
+    private val colorsRule = MaterialYouColorsRule()
+    private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
+    private val screenshotRule =
+        ScreenshotTestRule(
+            SettingsGoldenImagePathManager(
+                getEmulatedDevicePathConfig(emulationSpec),
+                assetsPathRelativeToBuildRoot
+            )
+        )
+    private val composeRule = createAndroidComposeRule<ComponentActivity>()
+    private val delegateRule =
+        RuleChain.outerRule(colorsRule)
+            .around(deviceEmulationRule)
+            .around(screenshotRule)
+            .around(composeRule)
+    private val matcher = UnitTestBitmapMatcher
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return delegateRule.apply(base, description)
+    }
+
+    /**
+     * Compare [content] with the golden image identified by [goldenIdentifier] in the context of
+     * [testSpec].
+     */
+    fun screenshotTest(
+        goldenIdentifier: String,
+        content: @Composable () -> Unit,
+    ) {
+        // Make sure that the activity draws full screen and fits the whole display.
+        val activity = composeRule.activity
+        activity.mainExecutor.execute { activity.window.setDecorFitsSystemWindows(false) }
+
+        // Set the content using the AndroidComposeRule to make sure that the Activity is set up
+        // correctly.
+        composeRule.setContent {
+            SettingsTheme {
+                Surface(
+                    color = MaterialTheme.colorScheme.background,
+                ) {
+                    content()
+                }
+            }
+        }
+        composeRule.waitForIdle()
+
+        val view = (composeRule.onRoot().fetchSemanticsNode().root as ViewRootForTest).view
+        screenshotRule.assertBitmapAgainstGolden(view.drawIntoBitmap(), goldenIdentifier, matcher)
+    }
+}
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/PreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/PreferenceScreenshotTest.kt
new file mode 100644
index 0000000..9631826
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/PreferenceScreenshotTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.settingslib.spa.screenshot
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Autorenew
+import androidx.compose.material.icons.outlined.DisabledByDefault
+import androidx.compose.runtime.Composable
+import com.android.settingslib.spa.framework.compose.toState
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.SettingsIcon
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import platform.test.screenshot.DeviceEmulationSpec
+
+/** A screenshot test for ExampleFeature. */
+@RunWith(Parameterized::class)
+class PreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletFull
+        private const val TITLE = "Title"
+        private const val SUMMARY = "Summary"
+        private const val LONG_SUMMARY =
+            "Long long long long long long long long long long long long long long long summary"
+    }
+
+    @get:Rule
+    val screenshotRule =
+        SettingsScreenshotTestRule(
+            emulationSpec,
+            "frameworks/base/packages/SettingsLib/Spa/screenshot/assets"
+        )
+
+    @Test
+    fun testPreference() {
+        screenshotRule.screenshotTest("preference") {
+            RegularScaffold(title = "Preference") {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                })
+
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val summary = SUMMARY.toState()
+                })
+
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val summary = LONG_SUMMARY.toState()
+                })
+
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val summary = SUMMARY.toState()
+                    override val enabled = false.toState()
+                    override val icon = @Composable {
+                        SettingsIcon(imageVector = Icons.Outlined.DisabledByDefault)
+                    }
+                })
+
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val summary = SUMMARY.toState()
+                    override val icon = @Composable {
+                        SettingsIcon(imageVector = Icons.Outlined.Autorenew)
+                    }
+                })
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index d3ba5e6..973d0de 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -774,6 +774,9 @@
     <!-- Permissions required for CTS test - CtsAppFgsTestCases -->
     <uses-permission android:name="android.permission.USE_EXACT_ALARM" />
 
+    <!-- Permission required for CTS test - CtsHardwareTestCases -->
+    <uses-permission android:name="android.permission.REMAP_MODIFIER_KEYS" />
+
     <!-- Permissions required for CTS test - CtsAppFgsTestCases -->
     <uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
     <uses-permission android:name="android.permission.health.READ_BASAL_BODY_TEMPERATURE" />
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
index 6d0cc5e..738b37c 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
@@ -34,7 +34,6 @@
 import platform.test.screenshot.DeviceEmulationRule
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.MaterialYouColorsRule
-import platform.test.screenshot.PathConfig
 import platform.test.screenshot.ScreenshotTestRule
 import platform.test.screenshot.getEmulatedDevicePathConfig
 import platform.test.screenshot.matchers.BitmapMatcher
@@ -43,7 +42,6 @@
 class ViewScreenshotTestRule(
     emulationSpec: DeviceEmulationSpec,
     private val matcher: BitmapMatcher = UnitTestBitmapMatcher,
-    pathConfig: PathConfig = getEmulatedDevicePathConfig(emulationSpec),
     assetsPathRelativeToBuildRoot: String
 ) : TestRule {
     private val colorsRule = MaterialYouColorsRule()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
index 142642a..802b9b6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
@@ -42,6 +42,7 @@
 import java.util.Optional
 import java.util.concurrent.Executor
 import javax.inject.Inject
+import javax.inject.Provider
 import kotlin.math.cos
 import kotlin.math.pow
 import kotlin.math.sin
@@ -64,7 +65,7 @@
     private val fingerprintManager: FingerprintManager?,
     private val handler: Handler,
     private val biometricExecutor: Executor,
-    private val alternateTouchProvider: Optional<AlternateUdfpsTouchProvider>,
+    private val alternateTouchProvider: Optional<Provider<AlternateUdfpsTouchProvider>>,
     @Main private val fgExecutor: DelayableExecutor,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val authController: AuthController,
@@ -126,6 +127,7 @@
                     if (!processedMotionEvent && goodOverlap) {
                         biometricExecutor.execute {
                             alternateTouchProvider
+                                .map(Provider<AlternateUdfpsTouchProvider>::get)
                                 .get()
                                 .onPointerDown(
                                     requestId,
@@ -142,7 +144,10 @@
 
                             view.configureDisplay {
                                 biometricExecutor.execute {
-                                    alternateTouchProvider.get().onUiReady()
+                                    alternateTouchProvider
+                                        .map(Provider<AlternateUdfpsTouchProvider>::get)
+                                        .get()
+                                        .onUiReady()
                                 }
                             }
 
@@ -158,7 +163,10 @@
             MotionEvent.ACTION_CANCEL -> {
                 if (processedMotionEvent && alternateTouchProvider.isPresent) {
                     biometricExecutor.execute {
-                        alternateTouchProvider.get().onPointerUp(requestId)
+                        alternateTouchProvider
+                            .map(Provider<AlternateUdfpsTouchProvider>::get)
+                            .get()
+                            .onPointerUp(requestId)
                     }
                     fgExecutor.execute {
                         if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
@@ -241,7 +249,10 @@
             if (overlayView != null && isShowing && alternateTouchProvider.isPresent) {
                 if (processedMotionEvent) {
                     biometricExecutor.execute {
-                        alternateTouchProvider.get().onPointerUp(requestId)
+                        alternateTouchProvider
+                            .map(Provider<AlternateUdfpsTouchProvider>::get)
+                            .get()
+                            .onPointerUp(requestId)
                     }
                     fgExecutor.execute {
                         if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
diff --git a/services/Android.bp b/services/Android.bp
index f6570e9..3f3ba06 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -187,6 +187,7 @@
         "framework-tethering.stubs.module_lib",
         "service-art.stubs.system_server",
         "service-permission.stubs.system_server",
+        "service-rkp.stubs.system_server",
         "service-sdksandbox.stubs.system_server",
     ],
 
diff --git a/services/core/Android.bp b/services/core/Android.bp
index b6f0237..1e1d610 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -132,6 +132,7 @@
         "framework-tethering.stubs.module_lib",
         "service-art.stubs.system_server",
         "service-permission.stubs.system_server",
+        "service-rkp.stubs.system_server",
         "service-sdksandbox.stubs.system_server",
     ],
     plugins: ["ImmutabilityAnnotationProcessor"],
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index 6cd7ce8..6f0971c 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -476,6 +476,7 @@
                 }
 
                 private void printPackageMeasurements(PackageInfo packageInfo,
+                                                      boolean useSha256,
                                                       final PrintWriter pw) {
                     Map<Integer, byte[]> contentDigests = computeApkContentDigest(
                             packageInfo.applicationInfo.sourceDir);
@@ -485,6 +486,14 @@
                         return;
                     }
 
+                    if (useSha256) {
+                        byte[] fileBuff = PackageUtils.createLargeFileBuffer();
+                        String hexEncodedSha256Digest =
+                                PackageUtils.computeSha256DigestForLargeFile(
+                                        packageInfo.applicationInfo.sourceDir, fileBuff);
+                        pw.print(hexEncodedSha256Digest + ",");
+                    }
+
                     for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) {
                         Integer algorithmId = entry.getKey();
                         byte[] contentDigest = entry.getValue();
@@ -497,6 +506,7 @@
                 }
 
                 private void printPackageInstallationInfo(PackageInfo packageInfo,
+                                                          boolean useSha256,
                                                           final PrintWriter pw) {
                     pw.println("--- Package Installation Info ---");
                     pw.println("Current install location: "
@@ -507,11 +517,13 @@
                         pw.println("|--> Pre-installed package install location: "
                                 + origPackageFilepath);
 
-                        // TODO(b/259347186): revive this with the proper cmd options.
-                        /*
-                        String digest = PackageUtils.computeSha256DigestForLargeFile(
-                        origPackageFilepath, PackageUtils.createLargeFileBuffer());
-                         */
+                        if (useSha256) {
+                            String sha256Digest = PackageUtils.computeSha256DigestForLargeFile(
+                                    origPackageFilepath, PackageUtils.createLargeFileBuffer());
+                            pw.println("|--> Pre-installed package SHA-256 digest: "
+                                    + sha256Digest);
+                        }
+
 
                         Map<Integer, byte[]> contentDigests = computeApkContentDigest(
                                 origPackageFilepath);
@@ -531,6 +543,8 @@
                     }
                     pw.println("First install time (ms): " + packageInfo.firstInstallTime);
                     pw.println("Last update time (ms): " + packageInfo.lastUpdateTime);
+                    // TODO(b/261493591): Determination of whether a package is preinstalled can be
+                    // made more robust
                     boolean isPreloaded = (packageInfo.firstInstallTime
                             == packageInfo.lastUpdateTime);
                     pw.println("Is preloaded: " + isPreloaded);
@@ -560,6 +574,7 @@
                         pw.println("ERROR: Package's signingInfo is null.");
                         return;
                     }
+                    // TODO(b/261501773): Handle printing of lineage of rotated keys.
                     pw.println("--- Package Signer Info ---");
                     pw.println("Has multiple signers: " + signerInfo.hasMultipleSigners());
                     Signature[] packageSigners = signerInfo.getApkContentsSigners();
@@ -669,15 +684,35 @@
 
                 }
 
+                private void printHeadersHelper(@NonNull String packageType,
+                                          boolean useSha256,
+                                          @NonNull final PrintWriter pw) {
+                    pw.print(packageType + " Info [Format: package_name,package_version,");
+                    if (useSha256) {
+                        pw.print("package_sha256_digest,");
+                    }
+                    pw.print("content_digest_algorithm:content_digest]:\n");
+                }
+
                 private int printAllApexs() {
                     final PrintWriter pw = getOutPrintWriter();
                     boolean verbose = false;
+                    boolean useSha256 = false;
+                    boolean printHeaders = true;
                     String opt;
                     while ((opt = getNextOption()) != null) {
                         switch (opt) {
                             case "-v":
+                            case "--verbose":
                                 verbose = true;
                                 break;
+                            case "-o":
+                            case "--old":
+                                useSha256 = true;
+                                break;
+                            case "--no-headers":
+                                printHeaders = false;
+                                break;
                             default:
                                 pw.println("ERROR: Unknown option: " + opt);
                                 return 1;
@@ -690,23 +725,17 @@
                         return -1;
                     }
 
-                    if (!verbose) {
-                        pw.println("APEX Info [Format: package_name,package_version,"
-                                // TODO(b/259347186): revive via special cmd line option
-                                //+ "package_sha256_digest,"
-                                + "content_digest_algorithm:content_digest]:");
+                    if (!verbose && printHeaders) {
+                        printHeadersHelper("APEX", useSha256, pw);
                     }
                     for (PackageInfo packageInfo : getCurrentInstalledApexs()) {
-                        if (verbose) {
-                            pw.println("APEX Info [Format: package_name,package_version,"
-                                    // TODO(b/259347186): revive via special cmd line option
-                                    //+ "package_sha256_digest,"
-                                    + "content_digest_algorithm:content_digest]:");
+                        if (verbose && printHeaders) {
+                            printHeadersHelper("APEX", useSha256, pw);
                         }
                         String packageName = packageInfo.packageName;
                         pw.print(packageName + ","
                                 + packageInfo.getLongVersionCode() + ",");
-                        printPackageMeasurements(packageInfo, pw);
+                        printPackageMeasurements(packageInfo, useSha256, pw);
 
                         if (verbose) {
                             ModuleInfo moduleInfo;
@@ -718,7 +747,7 @@
                                 pw.println("Is a module: false");
                             }
 
-                            printPackageInstallationInfo(packageInfo, pw);
+                            printPackageInstallationInfo(packageInfo, useSha256, pw);
                             printPackageSignerDetails(packageInfo.signingInfo, pw);
                             pw.println("");
                         }
@@ -729,12 +758,22 @@
                 private int printAllModules() {
                     final PrintWriter pw = getOutPrintWriter();
                     boolean verbose = false;
+                    boolean useSha256 = false;
+                    boolean printHeaders = true;
                     String opt;
                     while ((opt = getNextOption()) != null) {
                         switch (opt) {
                             case "-v":
+                            case "--verbose":
                                 verbose = true;
                                 break;
+                            case "-o":
+                            case "--old":
+                                useSha256 = true;
+                                break;
+                            case "--no-headers":
+                                printHeaders = false;
+                                break;
                             default:
                                 pw.println("ERROR: Unknown option: " + opt);
                                 return 1;
@@ -747,32 +786,25 @@
                         return -1;
                     }
 
-                    if (!verbose) {
-                        pw.println("Module Info [Format: package_name,package_version,"
-                                // TODO(b/259347186): revive via special cmd line option
-                                //+ "package_sha256_digest,"
-                                + "content_digest_algorithm:content_digest]:");
+                    if (!verbose && printHeaders) {
+                        printHeadersHelper("Module", useSha256, pw);
                     }
                     for (ModuleInfo module : pm.getInstalledModules(PackageManager.MATCH_ALL)) {
                         String packageName = module.getPackageName();
-                        if (verbose) {
-                            pw.println("Module Info [Format: package_name,package_version,"
-                                    // TODO(b/259347186): revive via special cmd line option
-                                    //+ "package_sha256_digest,"
-                                    + "content_digest_algorithm:content_digest]:");
+                        if (verbose && printHeaders) {
+                            printHeadersHelper("Module", useSha256, pw);
                         }
                         try {
                             PackageInfo packageInfo = pm.getPackageInfo(packageName,
                                     PackageManager.MATCH_APEX
                                             | PackageManager.GET_SIGNING_CERTIFICATES);
-                            //pw.print("package:");
                             pw.print(packageInfo.packageName + ",");
                             pw.print(packageInfo.getLongVersionCode() + ",");
-                            printPackageMeasurements(packageInfo, pw);
+                            printPackageMeasurements(packageInfo, useSha256, pw);
 
                             if (verbose) {
                                 printModuleDetails(module, pw);
-                                printPackageInstallationInfo(packageInfo, pw);
+                                printPackageInstallationInfo(packageInfo, useSha256, pw);
                                 printPackageSignerDetails(packageInfo.signingInfo, pw);
                                 pw.println("");
                             }
@@ -793,41 +825,45 @@
                     final PrintWriter pw = getOutPrintWriter();
                     boolean verbose = false;
                     boolean printLibraries = false;
+                    boolean useSha256 = false;
+                    boolean printHeaders = true;
                     String opt;
                     while ((opt = getNextOption()) != null) {
                         switch (opt) {
                             case "-v":
+                            case "--verbose":
                                 verbose = true;
                                 break;
                             case "-l":
                                 printLibraries = true;
                                 break;
+                            case "-o":
+                            case "--old":
+                                useSha256 = true;
+                                break;
+                            case "--no-headers":
+                                printHeaders = false;
+                                break;
                             default:
                                 pw.println("ERROR: Unknown option: " + opt);
                                 return 1;
                         }
                     }
 
-                    if (!verbose) {
-                        pw.println("MBA Info [Format: package_name,package_version,"
-                                // TODO(b/259347186): revive via special cmd line option
-                                //+ "package_sha256_digest,"
-                                + "content_digest_algorithm:content_digest]:");
+                    if (!verbose && printHeaders) {
+                        printHeadersHelper("MBA", useSha256, pw);
                     }
                     for (PackageInfo packageInfo : getNewlyInstalledMbas()) {
-                        if (verbose) {
-                            pw.println("MBA Info [Format: package_name,package_version,"
-                                    // TODO(b/259347186): revive via special cmd line option
-                                    //+ "package_sha256_digest,"
-                                    + "content_digest_algorithm:content_digest]:");
+                        if (verbose && printHeaders) {
+                            printHeadersHelper("MBA", useSha256, pw);
                         }
                         pw.print(packageInfo.packageName + ",");
                         pw.print(packageInfo.getLongVersionCode() + ",");
-                        printPackageMeasurements(packageInfo, pw);
+                        printPackageMeasurements(packageInfo, useSha256, pw);
 
                         if (verbose) {
                             printAppDetails(packageInfo, printLibraries, pw);
-                            printPackageInstallationInfo(packageInfo, pw);
+                            printPackageInstallationInfo(packageInfo, useSha256, pw);
                             printPackageSignerDetails(packageInfo.signingInfo, pw);
                             pw.println("");
                         }
@@ -894,27 +930,39 @@
                 private void printHelpMenu() {
                     final PrintWriter pw = getOutPrintWriter();
                     pw.println("Transparency manager (transparency) commands:");
-                    pw.println("    help");
-                    pw.println("        Print this help text.");
+                    pw.println("  help");
+                    pw.println("    Print this help text.");
                     pw.println("");
-                    pw.println("    get image_info [-a]");
-                    pw.println("        Print information about loaded image (firmware). Options:");
-                    pw.println("            -a: lists all other identifiable partitions.");
+                    pw.println("  get image_info [-a]");
+                    pw.println("    Print information about loaded image (firmware). Options:");
+                    pw.println("        -a: lists all other identifiable partitions.");
                     pw.println("");
-                    pw.println("    get apex_info [-v]");
-                    pw.println("        Print information about installed APEXs on device.");
-                    pw.println("            -v: lists more verbose information about each APEX.");
+                    pw.println("  get apex_info [-o] [-v] [--no-headers]");
+                    pw.println("    Print information about installed APEXs on device.");
+                    pw.println("      -o: also uses the old digest scheme (SHA256) to compute "
+                               + "APEX hashes. WARNING: This can be a very slow and CPU-intensive "
+                               + "computation.");
+                    pw.println("      -v: lists more verbose information about each APEX.");
+                    pw.println("      --no-headers: does not print the header if specified");
                     pw.println("");
-                    pw.println("    get module_info [-v]");
-                    pw.println("        Print information about installed modules on device.");
-                    pw.println("            -v: lists more verbose information about each module.");
+                    pw.println("  get module_info [-o] [-v] [--no-headers]");
+                    pw.println("    Print information about installed modules on device.");
+                    pw.println("      -o: also uses the old digest scheme (SHA256) to compute "
+                               + "module hashes. WARNING: This can be a very slow and "
+                               + "CPU-intensive computation.");
+                    pw.println("      -v: lists more verbose information about each module.");
+                    pw.println("      --no-headers: does not print the header if specified");
                     pw.println("");
-                    pw.println("    get mba_info [-v] [-l]");
-                    pw.println("        Print information about installed mobile bundle apps "
+                    pw.println("  get mba_info [-o] [-v] [-l] [--no-headers]");
+                    pw.println("    Print information about installed mobile bundle apps "
                                + "(MBAs on device).");
-                    pw.println("            -v: lists more verbose information about each app.");
-                    pw.println("            -l: lists shared library info. This will only be "
-                               + "listed with -v");
+                    pw.println("      -o: also uses the old digest scheme (SHA256) to compute "
+                               + "MBA hashes. WARNING: This can be a very slow and CPU-intensive "
+                               + "computation.");
+                    pw.println("      -v: lists more verbose information about each app.");
+                    pw.println("      -l: lists shared library info. (This option only works "
+                               + "when -v option is also specified)");
+                    pw.println("      --no-headers: does not print the header if specified");
                     pw.println("");
                 }
 
diff --git a/services/core/java/com/android/server/audio/TEST_MAPPING b/services/core/java/com/android/server/audio/TEST_MAPPING
index f3a73f0..8fbcb9a 100644
--- a/services/core/java/com/android/server/audio/TEST_MAPPING
+++ b/services/core/java/com/android/server/audio/TEST_MAPPING
@@ -13,6 +13,14 @@
                     "include-filter": "android.media.audio.cts.SpatializerTest"
                 }
             ]
+        },
+        {
+            "name": "AudioPolicyDeathTest",
+            "options": [
+                {
+                    "include-annotation": "android.platform.test.annotations.Presubmit"
+                }
+            ]
         }
     ]
 }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 81d782e..0da04a2 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -294,6 +294,9 @@
     // Manages Keyboard backlight
     private final KeyboardBacklightController mKeyboardBacklightController;
 
+    // Manages Keyboard modifier keys remapping
+    private final KeyRemapper mKeyRemapper;
+
     // Maximum number of milliseconds to wait for input event injection.
     private static final int INJECTION_TIMEOUT_MILLIS = 30 * 1000;
 
@@ -408,6 +411,7 @@
         mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
         mKeyboardBacklightController = new KeyboardBacklightController(mContext, mNative,
                 mDataStore, injector.getLooper());
+        mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
 
         mUseDevInputEventForAudioJack =
                 mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
@@ -536,6 +540,7 @@
         mKeyboardLayoutManager.systemRunning();
         mBatteryController.systemRunning();
         mKeyboardBacklightController.systemRunning();
+        mKeyRemapper.systemRunning();
     }
 
     private void reloadDeviceAliases() {
@@ -2738,6 +2743,27 @@
         return mKeyboardLayoutManager.getKeyboardLayoutOverlay(identifier);
     }
 
+    @EnforcePermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+    @Override // Binder call
+    public void remapModifierKey(int fromKey, int toKey) {
+        super.remapModifierKey_enforcePermission();
+        mKeyRemapper.remapKey(fromKey, toKey);
+    }
+
+    @EnforcePermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+    @Override // Binder call
+    public void clearAllModifierKeyRemappings() {
+        super.clearAllModifierKeyRemappings_enforcePermission();
+        mKeyRemapper.clearAllKeyRemappings();
+    }
+
+    @EnforcePermission(Manifest.permission.REMAP_MODIFIER_KEYS)
+    @Override // Binder call
+    public Map<Integer, Integer> getModifierKeyRemapping() {
+        super.getModifierKeyRemapping_enforcePermission();
+        return mKeyRemapper.getKeyRemapping();
+    }
+
     // Native callback.
     @SuppressWarnings("unused")
     private String getDeviceAlias(String uniqueId) {
diff --git a/services/core/java/com/android/server/input/KeyRemapper.java b/services/core/java/com/android/server/input/KeyRemapper.java
new file mode 100644
index 0000000..950e094
--- /dev/null
+++ b/services/core/java/com/android/server/input/KeyRemapper.java
@@ -0,0 +1,161 @@
+/*
+ * 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.input;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.InputDevice;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A component of {@link InputManagerService} responsible for managing key remappings.
+ *
+ * @hide
+ */
+final class KeyRemapper implements InputManager.InputDeviceListener {
+
+    private static final int MSG_UPDATE_EXISTING_DEVICES = 1;
+    private static final int MSG_REMAP_KEY = 2;
+    private static final int MSG_CLEAR_ALL_REMAPPING = 3;
+
+    private final Context mContext;
+    private final NativeInputManagerService mNative;
+    // The PersistentDataStore should be locked before use.
+    @GuardedBy("mDataStore")
+    private final PersistentDataStore mDataStore;
+    private final Handler mHandler;
+
+    KeyRemapper(Context context, NativeInputManagerService nativeService,
+            PersistentDataStore dataStore, Looper looper) {
+        mContext = context;
+        mNative = nativeService;
+        mDataStore = dataStore;
+        mHandler = new Handler(looper, this::handleMessage);
+    }
+
+    public void systemRunning() {
+        InputManager inputManager = Objects.requireNonNull(
+                mContext.getSystemService(InputManager.class));
+        inputManager.registerInputDeviceListener(this, mHandler);
+
+        Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES,
+                inputManager.getInputDeviceIds());
+        mHandler.sendMessage(msg);
+    }
+
+    public void remapKey(int fromKey, int toKey) {
+        Message msg = Message.obtain(mHandler, MSG_REMAP_KEY, fromKey, toKey);
+        mHandler.sendMessage(msg);
+    }
+
+    public void clearAllKeyRemappings() {
+        Message msg = Message.obtain(mHandler, MSG_CLEAR_ALL_REMAPPING);
+        mHandler.sendMessage(msg);
+    }
+
+    public Map<Integer, Integer> getKeyRemapping() {
+        synchronized (mDataStore) {
+            return mDataStore.getKeyRemapping();
+        }
+    }
+
+    private void addKeyRemapping(int fromKey, int toKey) {
+        InputManager inputManager = Objects.requireNonNull(
+                mContext.getSystemService(InputManager.class));
+        for (int deviceId : inputManager.getInputDeviceIds()) {
+            InputDevice inputDevice = inputManager.getInputDevice(deviceId);
+            if (inputDevice != null && !inputDevice.isVirtual() && inputDevice.isFullKeyboard()) {
+                mNative.addKeyRemapping(deviceId, fromKey, toKey);
+            }
+        }
+    }
+
+    private void remapKeyInternal(int fromKey, int toKey) {
+        addKeyRemapping(fromKey, toKey);
+        synchronized (mDataStore) {
+            try {
+                if (fromKey == toKey) {
+                    mDataStore.clearMappedKey(fromKey);
+                } else {
+                    mDataStore.remapKey(fromKey, toKey);
+                }
+            } finally {
+                mDataStore.saveIfNeeded();
+            }
+        }
+    }
+
+    private void clearAllRemappingsInternal() {
+        synchronized (mDataStore) {
+            try {
+                Map<Integer, Integer> keyRemapping = mDataStore.getKeyRemapping();
+                for (int fromKey : keyRemapping.keySet()) {
+                    mDataStore.clearMappedKey(fromKey);
+
+                    // Remapping to itself will clear the remapping on native side
+                    addKeyRemapping(fromKey, fromKey);
+                }
+            } finally {
+                mDataStore.saveIfNeeded();
+            }
+        }
+    }
+
+    @Override
+    public void onInputDeviceAdded(int deviceId) {
+        InputManager inputManager = Objects.requireNonNull(
+                mContext.getSystemService(InputManager.class));
+        InputDevice inputDevice = inputManager.getInputDevice(deviceId);
+        if (inputDevice != null && !inputDevice.isVirtual() && inputDevice.isFullKeyboard()) {
+            Map<Integer, Integer> remapping = getKeyRemapping();
+            remapping.forEach(
+                    (fromKey, toKey) -> mNative.addKeyRemapping(deviceId, fromKey, toKey));
+        }
+    }
+
+    @Override
+    public void onInputDeviceRemoved(int deviceId) {
+    }
+
+    @Override
+    public void onInputDeviceChanged(int deviceId) {
+    }
+
+    private boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_UPDATE_EXISTING_DEVICES:
+                for (int deviceId : (int[]) msg.obj) {
+                    onInputDeviceAdded(deviceId);
+                }
+                return true;
+            case MSG_REMAP_KEY:
+                remapKeyInternal(msg.arg1, msg.arg2);
+                return true;
+            case MSG_CLEAR_ALL_REMAPPING:
+                clearAllRemappingsInternal();
+                return true;
+        }
+        return false;
+    }
+}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index cfa7fb1..8781c6e 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -47,6 +47,8 @@
 
     int getSwitchState(int deviceId, int sourceMask, int sw);
 
+    void addKeyRemapping(int deviceId, int fromKeyCode, int toKeyCode);
+
     boolean hasKeys(int deviceId, int sourceMask, int[] keyCodes, boolean[] keyExists);
 
     int getKeyCodeForKeyLocation(int deviceId, int locationKeyCode);
@@ -235,6 +237,9 @@
         public native int getSwitchState(int deviceId, int sourceMask, int sw);
 
         @Override
+        public native void addKeyRemapping(int deviceId, int fromKeyCode, int toKeyCode);
+
+        @Override
         public native boolean hasKeys(int deviceId, int sourceMask, int[] keyCodes,
                 boolean[] keyExists);
 
diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java
index 1bb10c7..375377a7 100644
--- a/services/core/java/com/android/server/input/PersistentDataStore.java
+++ b/services/core/java/com/android/server/input/PersistentDataStore.java
@@ -27,14 +27,13 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
 import org.xmlpull.v1.XmlPullParserException;
 
-import com.android.modules.utils.TypedXmlPullParser;
-import com.android.modules.utils.TypedXmlSerializer;
-
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -64,6 +63,8 @@
 final class PersistentDataStore {
     static final String TAG = "InputManager";
 
+    private static final int INVALID_VALUE = -1;
+
     // Input device state by descriptor.
     private final HashMap<String, InputDeviceState> mInputDevices =
             new HashMap<String, InputDeviceState>();
@@ -77,6 +78,9 @@
     // True if there are changes to be saved.
     private boolean mDirty;
 
+    // Storing key remapping
+    private Map<Integer, Integer> mKeyRemapping = new HashMap<>();
+
     public PersistentDataStore() {
         this(new Injector());
     }
@@ -187,6 +191,30 @@
         return state.getKeyboardBacklightBrightness(lightId);
     }
 
+    public boolean remapKey(int fromKey, int toKey) {
+        loadIfNeeded();
+        if (mKeyRemapping.getOrDefault(fromKey, INVALID_VALUE) == toKey) {
+            return false;
+        }
+        mKeyRemapping.put(fromKey, toKey);
+        setDirty();
+        return true;
+    }
+
+    public boolean clearMappedKey(int key) {
+        loadIfNeeded();
+        if (mKeyRemapping.containsKey(key)) {
+            mKeyRemapping.remove(key);
+            setDirty();
+        }
+        return true;
+    }
+
+    public Map<Integer, Integer> getKeyRemapping() {
+        loadIfNeeded();
+        return new HashMap<>(mKeyRemapping);
+    }
+
     public boolean removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts) {
         boolean changed = false;
         for (InputDeviceState state : mInputDevices.values()) {
@@ -229,6 +257,7 @@
     }
 
     private void clearState() {
+        mKeyRemapping.clear();
         mInputDevices.clear();
     }
 
@@ -280,7 +309,9 @@
         XmlUtils.beginDocument(parser, "input-manager-state");
         final int outerDepth = parser.getDepth();
         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
-            if (parser.getName().equals("input-devices")) {
+            if (parser.getName().equals("key-remapping")) {
+                loadKeyRemappingFromXml(parser);
+            } else if (parser.getName().equals("input-devices")) {
                 loadInputDevicesFromXml(parser);
             }
         }
@@ -307,10 +338,31 @@
         }
     }
 
+    private void loadKeyRemappingFromXml(TypedXmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        final int outerDepth = parser.getDepth();
+        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+            if (parser.getName().equals("remap")) {
+                int fromKey = parser.getAttributeInt(null, "from-key");
+                int toKey = parser.getAttributeInt(null, "to-key");
+                mKeyRemapping.put(fromKey, toKey);
+            }
+        }
+    }
+
     private void saveToXml(TypedXmlSerializer serializer) throws IOException {
         serializer.startDocument(null, true);
         serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
         serializer.startTag(null, "input-manager-state");
+        serializer.startTag(null, "key-remapping");
+        for (int fromKey : mKeyRemapping.keySet()) {
+            int toKey = mKeyRemapping.get(fromKey);
+            serializer.startTag(null, "remap");
+            serializer.attributeInt(null, "from-key", fromKey);
+            serializer.attributeInt(null, "to-key", toKey);
+            serializer.endTag(null, "remap");
+        }
+        serializer.endTag(null, "key-remapping");
         serializer.startTag(null, "input-devices");
         for (Map.Entry<String, InputDeviceState> entry : mInputDevices.entrySet()) {
             final String descriptor = entry.getKey();
@@ -329,7 +381,6 @@
         private static final String[] CALIBRATION_NAME = { "x_scale",
                 "x_ymix", "x_offset", "y_xmix", "y_scale", "y_offset" };
 
-        private static final int INVALID_VALUE = -1;
         private final TouchCalibration[] mTouchCalibration = new TouchCalibration[4];
         @Nullable
         private String mCurrentKeyboardLayout;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 409d352..4803c5e 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -154,6 +154,11 @@
     /** Destroy sessions older than this on storage free request */
     private static final long MAX_SESSION_AGE_ON_LOW_STORAGE_MILLIS = 8 * DateUtils.HOUR_IN_MILLIS;
 
+    /** Threshold of historical sessions size */
+    private static final int HISTORICAL_SESSIONS_THRESHOLD = 500;
+    /** Size of historical sessions to be cleared when reaching threshold */
+    private static final int HISTORICAL_CLEAR_SIZE = 400;
+
     /**
      * Allow verification-skipping if it's a development app installed through ADB with
      * disable verification flag specified.
@@ -549,6 +554,10 @@
         CharArrayWriter writer = new CharArrayWriter();
         IndentingPrintWriter pw = new IndentingPrintWriter(writer, "    ");
         session.dump(pw);
+        if (mHistoricalSessions.size() > HISTORICAL_SESSIONS_THRESHOLD) {
+            Slog.d(TAG, "Historical sessions size reaches threshold, clear the oldest");
+            mHistoricalSessions.subList(0, HISTORICAL_CLEAR_SIZE).clear();
+        }
         mHistoricalSessions.add(writer.toString());
 
         int installerUid = session.getInstallerUid();
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 809911a..89b74f4 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -2470,7 +2470,7 @@
                 mReadMessages.append("Reading from backup stopped packages file\n");
                 PackageManagerService.reportSettingsProblem(Log.INFO,
                         "Need to read from backup stopped packages file");
-                if (mSettingsFilename.exists()) {
+                if (mStoppedPackagesFilename.exists()) {
                     // If both the backup and normal file exist, we
                     // ignore the normal one since it might have been
                     // corrupted.
diff --git a/services/core/java/com/android/server/security/rkp/OWNERS b/services/core/java/com/android/server/security/rkp/OWNERS
new file mode 100644
index 0000000..348f940
--- /dev/null
+++ b/services/core/java/com/android/server/security/rkp/OWNERS
@@ -0,0 +1 @@
+file:platform/frameworks/base:master:/core/java/android/security/rkp/OWNERS
diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
new file mode 100644
index 0000000..65a4b38
--- /dev/null
+++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
@@ -0,0 +1,126 @@
+/*
+ * 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.security.rkp;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.security.rkp.IGetKeyCallback;
+import android.security.rkp.IGetRegistrationCallback;
+import android.security.rkp.IRegistration;
+import android.security.rkp.IRemoteProvisioning;
+import android.security.rkp.service.RegistrationProxy;
+import android.util.Log;
+
+import com.android.server.SystemService;
+
+import java.time.Duration;
+
+/**
+ * Implements the remote provisioning system service. This service is backed by a mainline
+ * module, allowing the underlying implementation to be updated. The code here is a thin
+ * proxy for the code in android.security.rkp.service.
+ *
+ * @hide
+ */
+public class RemoteProvisioningService extends SystemService {
+    public static final String TAG = "RemoteProvisionSysSvc";
+    private static final Duration CREATE_REGISTRATION_TIMEOUT = Duration.ofSeconds(10);
+    private final RemoteProvisioningImpl mBinderImpl = new RemoteProvisioningImpl();
+
+    /** @hide */
+    public RemoteProvisioningService(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(Context.REMOTE_PROVISIONING_SERVICE, mBinderImpl);
+    }
+
+    private final class RemoteProvisioningImpl extends IRemoteProvisioning.Stub {
+
+        final class RegistrationBinder extends IRegistration.Stub {
+            static final String TAG = RemoteProvisioningService.TAG;
+            private final RegistrationProxy mRegistration;
+
+            RegistrationBinder(RegistrationProxy registration) {
+                mRegistration = registration;
+            }
+
+            @Override
+            public void getKey(int keyId, IGetKeyCallback callback) {
+                Log.e(TAG, "RegistrationBinder.getKey NOT YET IMPLEMENTED");
+            }
+
+            @Override
+            public void cancelGetKey(IGetKeyCallback callback) {
+                Log.e(TAG, "RegistrationBinder.cancelGetKey NOT YET IMPLEMENTED");
+            }
+
+            @Override
+            public void storeUpgradedKey(byte[] oldKeyBlob, byte[] newKeyBlob) {
+                Log.e(TAG, "RegistrationBinder.storeUpgradedKey NOT YET IMPLEMENTED");
+            }
+        }
+
+        @Override
+        public void getRegistration(String irpcName, IGetRegistrationCallback callback)
+                throws RemoteException {
+            final int callerUid = Binder.getCallingUidOrThrow();
+            final long callingIdentity = Binder.clearCallingIdentity();
+            try {
+                Log.i(TAG, "getRegistration(" + irpcName + ")");
+                RegistrationProxy.createAsync(
+                        getContext(),
+                        callerUid,
+                        irpcName,
+                        CREATE_REGISTRATION_TIMEOUT,
+                        getContext().getMainExecutor(),
+                        new OutcomeReceiver<>() {
+                            @Override
+                            public void onResult(RegistrationProxy registration) {
+                                try {
+                                    callback.onSuccess(new RegistrationBinder(registration));
+                                } catch (RemoteException e) {
+                                    Log.e(TAG, "Error calling success callback", e);
+                                }
+                            }
+
+                            @Override
+                            public void onError(Exception error) {
+                                try {
+                                    callback.onError(error.toString());
+                                } catch (RemoteException e) {
+                                    Log.e(TAG, "Error calling error callback", e);
+                                }
+                            }
+                        });
+            } finally {
+                Binder.restoreCallingIdentity(callingIdentity);
+            }
+        }
+
+        @Override
+        public void cancelGetRegistration(IGetRegistrationCallback callback)
+                throws RemoteException {
+            Log.i(TAG, "cancelGetRegistration()");
+            callback.onError("cancelGetRegistration not yet implemented");
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 5d08461..4480d52 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -34,6 +34,7 @@
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
+import android.app.ActivityOptions;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
 import android.app.ILocalWallpaperColorConsumer;
@@ -3166,6 +3167,15 @@
                 }
             }
 
+            final ActivityOptions clientOptions = ActivityOptions.makeBasic();
+            clientOptions.setIgnorePendingIntentCreatorForegroundState(true);
+            PendingIntent clientIntent = PendingIntent.getActivityAsUser(
+                    mContext, 0, Intent.createChooser(
+                            new Intent(Intent.ACTION_SET_WALLPAPER),
+                            mContext.getText(com.android.internal.R.string.chooser_wallpaper)),
+                    PendingIntent.FLAG_IMMUTABLE, clientOptions.toBundle(),
+                    UserHandle.of(serviceUserId));
+
             // Bind the service!
             if (DEBUG) Slog.v(TAG, "Binding to:" + componentName);
             final int componentUid = mIPackageManager.getPackageUid(componentName.getPackageName(),
@@ -3174,11 +3184,7 @@
             intent.setComponent(componentName);
             intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
                     com.android.internal.R.string.wallpaper_binding_label);
-            intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivityAsUser(
-                    mContext, 0,
-                    Intent.createChooser(new Intent(Intent.ACTION_SET_WALLPAPER),
-                            mContext.getText(com.android.internal.R.string.chooser_wallpaper)),
-                    PendingIntent.FLAG_IMMUTABLE, null, new UserHandle(serviceUserId)));
+            intent.putExtra(Intent.EXTRA_CLIENT_INTENT, clientIntent);
             if (!mContext.bindServiceAsUser(intent, newConn,
                     Context.BIND_AUTO_CREATE | Context.BIND_SHOWING_UI
                             | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index 707d0044..e145898 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -38,6 +38,7 @@
 import android.util.Slog;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.infra.AbstractMasterSystemService;
@@ -161,7 +162,7 @@
         return null;
     }
 
-    // Used in testing.
+    @VisibleForTesting
     void provideDataStream(@UserIdInt int userId, ParcelFileDescriptor parcelFileDescriptor,
             RemoteCallback callback) {
         synchronized (mLock) {
@@ -174,7 +175,7 @@
         }
     }
 
-    // Used in testing.
+    @VisibleForTesting
     void provideData(@UserIdInt int userId, PersistableBundle data, SharedMemory sharedMemory,
             RemoteCallback callback) {
         synchronized (mLock) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 969056e..145e088 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -1578,6 +1578,12 @@
     return vec;
 }
 
+static void nativeAddKeyRemapping(JNIEnv* env, jobject nativeImplObj, jint deviceId,
+                                  jint fromKeyCode, jint toKeyCode) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+    im->getInputManager()->getReader().addKeyRemapping(deviceId, fromKeyCode, toKeyCode);
+}
+
 static jboolean nativeHasKeys(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint sourceMask,
                               jintArray keyCodes, jbooleanArray outFlags) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2360,6 +2366,7 @@
         {"getScanCodeState", "(III)I", (void*)nativeGetScanCodeState},
         {"getKeyCodeState", "(III)I", (void*)nativeGetKeyCodeState},
         {"getSwitchState", "(III)I", (void*)nativeGetSwitchState},
+        {"addKeyRemapping", "(III)V", (void*)nativeAddKeyRemapping},
         {"hasKeys", "(II[I[Z)Z", (void*)nativeHasKeys},
         {"getKeyCodeForKeyLocation", "(II)I", (void*)nativeGetKeyCodeForKeyLocation},
         {"createInputChannel", "(Ljava/lang/String;)Landroid/view/InputChannel;",
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index e96eff21..e41e781 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -187,6 +187,7 @@
 import com.android.server.security.FileIntegrityService;
 import com.android.server.security.KeyAttestationApplicationIdProviderService;
 import com.android.server.security.KeyChainSystemService;
+import com.android.server.security.rkp.RemoteProvisioningService;
 import com.android.server.sensorprivacy.SensorPrivacyService;
 import com.android.server.sensors.SensorService;
 import com.android.server.signedconfig.SignedConfigService;
@@ -1361,11 +1362,16 @@
         mSystemServiceManager.startService(BugreportManagerService.class);
         t.traceEnd();
 
-        // Serivce for GPU and GPU driver.
+        // Service for GPU and GPU driver.
         t.traceBegin("GpuService");
         mSystemServiceManager.startService(GpuService.class);
         t.traceEnd();
 
+        // Handles system process requests for remotely provisioned keys & data.
+        t.traceBegin("StartRemoteProvisioningService");
+        mSystemServiceManager.startService(RemoteProvisioningService.class);
+        t.traceEnd();
+
         t.traceEnd(); // startCoreServices
     }
 
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index 939fb6a..70a5c3f 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -28,7 +28,7 @@
     ],
 
     srcs: [
-        "src/**/*.java",
+        "src/server/**/*.java",
     ],
 
     static_libs: [
@@ -60,3 +60,52 @@
         enabled: false,
     },
 }
+
+android_test {
+    name: "FrameworksImeTests",
+    defaults: [
+        "modules-utils-testable-device-config-defaults",
+    ],
+
+    srcs: [
+        "src/com/android/inputmethodservice/**/*.java",
+    ],
+
+    manifest: "src/com/android/inputmethodservice/AndroidManifest.xml",
+    test_config: "src/com/android/inputmethodservice/AndroidTest.xml",
+
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.runner",
+        "androidx.test.espresso.core",
+        "androidx.test.espresso.contrib",
+        "androidx.test.ext.truth",
+        "frameworks-base-testutils",
+        "mockito-target-extended-minus-junit4",
+        "platform-test-annotations",
+        "services.core",
+        "servicestests-core-utils",
+        "servicestests-utils-mockito-extended",
+        "truth-prebuilt",
+        "SimpleImeTestingLib",
+        "SimpleImeImsLib",
+    ],
+
+    libs: [
+        "android.test.mock",
+        "android.test.base",
+        "android.test.runner",
+    ],
+
+    data: [
+        ":SimpleTestIme",
+    ],
+
+    certificate: "platform",
+    platform_apis: true,
+    test_suites: ["device-tests"],
+
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
new file mode 100644
index 0000000..0104f71
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.inputmethod.imetests">
+
+    <uses-sdk android:targetSdkVersion="31" />
+
+    <!-- Permissions required for granting and logging -->
+    <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
+    <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
+    <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"/>
+    <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD"/>
+
+    <!-- Permissions for reading system info -->
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <!-- The "targetPackage" reference the instruments APK package, which is the FakeImeApk, while
+    the test package is "com.android.inputmethod.imetests" (FrameworksImeTests.apk).-->
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.apps.inputmethod.simpleime"
+        android:label="Frameworks IME Tests" />
+</manifest>
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml
new file mode 100644
index 0000000..6c24d6d
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml
@@ -0,0 +1,41 @@
+<?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.
+  -->
+<configuration description="Runs Frameworks IME Tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="FrameworksImeTests.apk" />
+        <option name="test-file-name" value="SimpleTestIme.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="FrameworksImeTests" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.inputmethod.imetests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+
+    <!-- Collect the files in the dump directory for debugging -->
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/sdcard/FrameworksImeTests/" />
+        <option name="collect-on-run-ended-only" value="true" />
+    </metrics_collector>
+</configuration>
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
new file mode 100644
index 0000000..16a9845
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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.inputmethodservice;
+
+import static com.android.compatibility.common.util.SystemUtil.eventually;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.RemoteException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper;
+import com.android.apps.inputmethod.simpleime.testing.TestActivity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class InputMethodServiceTest {
+    private static final String TAG = "SimpleIMSTest";
+    private static final String INPUT_METHOD_SERVICE_NAME = ".SimpleInputMethodService";
+    private static final String EDIT_TEXT_DESC = "Input box";
+    private static final long TIMEOUT_IN_SECONDS = 3;
+
+    public Instrumentation mInstrumentation;
+    public UiDevice mUiDevice;
+    public Context mContext;
+    public String mTargetPackageName;
+    public TestActivity mActivity;
+    public EditText mEditText;
+    public InputMethodServiceWrapper mInputMethodService;
+    public String mInputMethodId;
+
+    @Before
+    public void setUp() throws Exception {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mUiDevice = UiDevice.getInstance(mInstrumentation);
+        mContext = mInstrumentation.getContext();
+        mTargetPackageName = mInstrumentation.getTargetContext().getPackageName();
+        mInputMethodId = getInputMethodId();
+        prepareIme();
+        prepareEditor();
+
+        // Waits for input binding ready.
+        eventually(
+                () -> {
+                    mInputMethodService =
+                            InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting();
+                    assertThat(mInputMethodService).isNotNull();
+
+                    // The editor won't bring up keyboard by default.
+                    assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
+                    assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse();
+                });
+    }
+
+    @Test
+    public void testShowHideKeyboard_byUserAction() throws InterruptedException {
+        // Performs click on editor box to bring up the soft keyboard.
+        Log.i(TAG, "Click on EditText.");
+        verifyInputViewStatus(() -> clickOnEditorText(), true /* inputViewStarted */);
+
+        // Press back key to hide soft keyboard.
+        Log.i(TAG, "Press back");
+        verifyInputViewStatus(
+                () -> assertThat(mUiDevice.pressHome()).isTrue(), false /* inputViewStarted */);
+    }
+
+    @Test
+    public void testShowHideKeyboard_byApi() throws InterruptedException {
+        // Triggers to show IME via public API.
+        verifyInputViewStatus(
+                () -> assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(),
+                true /* inputViewStarted */);
+
+        // Triggers to hide IME via public API.
+        // TODO(b/242838873): investigate why WIC#hide(ime()) does not work, likely related to
+        //  triggered from IME process.
+        verifyInputViewStatusOnMainSync(
+                () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(),
+                false /* inputViewStarted */);
+    }
+
+    @Test
+    public void testShowHideSelf() throws InterruptedException {
+        // IME requests to show itself without any flags: expect shown.
+        Log.i(TAG, "Call IMS#requestShowSelf(0)");
+        verifyInputViewStatusOnMainSync(
+                () -> mInputMethodService.requestShowSelf(0), true /* inputViewStarted */);
+
+        // IME requests to hide itself with flag: HIDE_IMPLICIT_ONLY, expect not hide (shown).
+        Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
+        verifyInputViewStatusOnMainSync(
+                () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
+                true /* inputViewStarted */);
+
+        // IME request to hide itself without any flags: expect hidden.
+        Log.i(TAG, "Call IMS#requestHideSelf(0)");
+        verifyInputViewStatusOnMainSync(
+                () -> mInputMethodService.requestHideSelf(0), false /* inputViewStarted */);
+
+        // IME request to show itself with flag SHOW_IMPLICIT: expect shown.
+        Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
+        verifyInputViewStatusOnMainSync(
+                () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
+                true /* inputViewStarted */);
+
+        // IME request to hide itself with flag: HIDE_IMPLICIT_ONLY, expect hidden.
+        Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
+        verifyInputViewStatusOnMainSync(
+                () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
+                false /* inputViewStarted */);
+    }
+
+    private void verifyInputViewStatus(Runnable runnable, boolean inputViewStarted)
+            throws InterruptedException {
+        verifyInputViewStatusInternal(runnable, inputViewStarted, false /*runOnMainSync*/);
+    }
+
+    private void verifyInputViewStatusOnMainSync(Runnable runnable, boolean inputViewStarted)
+            throws InterruptedException {
+        verifyInputViewStatusInternal(runnable, inputViewStarted, true /*runOnMainSync*/);
+    }
+
+    private void verifyInputViewStatusInternal(
+            Runnable runnable, boolean inputViewStarted, boolean runOnMainSync)
+            throws InterruptedException {
+        CountDownLatch signal = new CountDownLatch(1);
+        mInputMethodService.setCountDownLatchForTesting(signal);
+        // Runnable to trigger onStartInputView()/ onFinishInputView()
+        if (runOnMainSync) {
+            mInstrumentation.runOnMainSync(runnable);
+        } else {
+            runnable.run();
+        }
+        // Waits for onStartInputView() to finish.
+        mInstrumentation.waitForIdleSync();
+        signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+        // Input is not finished.
+        assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
+        assertThat(mInputMethodService.getCurrentInputViewStarted()).isEqualTo(inputViewStarted);
+    }
+
+    @Test
+    public void testFullScreenMode() throws Exception {
+        Log.i(TAG, "Set orientation natural");
+        verifyFullscreenMode(() -> setOrientation(0), true /* orientationPortrait */);
+
+        Log.i(TAG, "Set orientation left");
+        verifyFullscreenMode(() -> setOrientation(1), false /* orientationPortrait */);
+
+        Log.i(TAG, "Set orientation right");
+        verifyFullscreenMode(() -> setOrientation(2), false /* orientationPortrait */);
+
+        mUiDevice.unfreezeRotation();
+    }
+
+    private void setOrientation(int orientation) {
+        // Simple wrapper for catching RemoteException.
+        try {
+            switch (orientation) {
+                case 1:
+                    mUiDevice.setOrientationLeft();
+                    break;
+                case 2:
+                    mUiDevice.setOrientationRight();
+                    break;
+                default:
+                    mUiDevice.setOrientationNatural();
+            }
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void verifyFullscreenMode(Runnable runnable, boolean orientationPortrait)
+            throws InterruptedException {
+        CountDownLatch signal = new CountDownLatch(1);
+        mInputMethodService.setCountDownLatchForTesting(signal);
+
+        // Runnable to trigger onConfigurationChanged()
+        try {
+            runnable.run();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        // Waits for onConfigurationChanged() to finish.
+        mInstrumentation.waitForIdleSync();
+        signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+
+        clickOnEditorText();
+        eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isTrue());
+
+        assertThat(mInputMethodService.getResources().getConfiguration().orientation)
+                .isEqualTo(
+                        orientationPortrait
+                                ? Configuration.ORIENTATION_PORTRAIT
+                                : Configuration.ORIENTATION_LANDSCAPE);
+        EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
+        assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN).isEqualTo(0);
+        assertThat(editorInfo.internalImeOptions & EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT)
+                .isEqualTo(
+                        orientationPortrait ? EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT : 0);
+        assertThat(mInputMethodService.onEvaluateFullscreenMode()).isEqualTo(!orientationPortrait);
+        assertThat(mInputMethodService.isFullscreenMode()).isEqualTo(!orientationPortrait);
+
+        mUiDevice.pressBack();
+    }
+
+    private void prepareIme() throws Exception {
+        executeShellCommand("ime enable " + mInputMethodId);
+        executeShellCommand("ime set " + mInputMethodId);
+        mInstrumentation.waitForIdleSync();
+        Log.i(TAG, "Finish preparing IME");
+    }
+
+    private void prepareEditor() {
+        mActivity = TestActivity.start(mInstrumentation);
+        mEditText = mActivity.mEditText;
+        Log.i(TAG, "Finish preparing activity with editor.");
+    }
+
+    private String getInputMethodId() {
+        return mTargetPackageName + "/" + INPUT_METHOD_SERVICE_NAME;
+    }
+
+    private String executeShellCommand(String cmd) throws Exception {
+        Log.i(TAG, "Run command: " + cmd);
+        return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+                .executeShellCommand(cmd);
+    }
+
+    private void clickOnEditorText() {
+        // Find the editText and click it.
+        UiObject2 editTextUiObject =
+                mUiDevice.wait(
+                        Until.findObject(By.desc(EDIT_TEXT_DESC)),
+                        TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS));
+        assertThat(editTextUiObject).isNotNull();
+        editTextUiObject.click();
+        mInstrumentation.waitForIdleSync();
+    }
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
new file mode 100644
index 0000000..ef50476
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
@@ -0,0 +1,62 @@
+// 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test_helper_app {
+    name: "SimpleTestIme",
+
+    srcs: [
+        "src/com/android/apps/inputmethod/simpleime/*.java",
+    ],
+
+    static_libs: [
+        "SimpleImeImsLib",
+        "SimpleImeTestingLib",
+    ],
+    resource_dirs: ["res"],
+    manifest: "AndroidManifest.xml",
+
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: false,
+    },
+    export_package_resources: true,
+    sdk_version: "current",
+}
+
+android_library {
+    name: "SimpleImeImsLib",
+    srcs: [
+        "src/com/android/apps/inputmethod/simpleime/ims/*.java",
+    ],
+    sdk_version: "current",
+}
+
+android_library {
+    name: "SimpleImeTestingLib",
+    srcs: [
+        "src/com/android/apps/inputmethod/simpleime/testing/*.java",
+    ],
+    sdk_version: "current",
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
new file mode 100644
index 0000000..802caf1
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.apps.inputmethod.simpleime">
+
+    <uses-sdk android:targetSdkVersion="31" />
+
+    <application android:debuggable="true"
+                 android:label="@string/app_name">
+        <service
+            android:name="com.android.apps.inputmethod.simpleime.SimpleInputMethodService"
+            android:label="@string/app_name"
+            android:directBootAware="true"
+            android:permission="android.permission.BIND_INPUT_METHOD"
+            android:exported="true">
+
+            <meta-data
+                android:name="android.view.im"
+                android:resource="@xml/method"/>
+
+            <intent-filter>
+                <action android:name="android.view.InputMethod"/>
+            </intent-filter>
+        </service>
+
+        <!-- This is for test only. -->
+        <activity android:name="com.android.apps.inputmethod.simpleime.testing.TestActivity"
+                  android:exported="false"
+                  android:label="TestActivity"
+                  android:launchMode="singleInstance"
+                  android:excludeFromRecents="true"
+                  android:noHistory="true"
+                  android:taskAffinity="">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/drawable/key_border.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/drawable/key_border.xml
new file mode 100644
index 0000000..dbfcc30
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/drawable/key_border.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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle" >
+    <solid
+        android:color="#FAFAFA" >
+    </solid>
+    <stroke
+        android:width="1dp"
+        android:color="#0F000000" >
+    </stroke>
+    <corners
+        android:radius="2dp"   >
+    </corners>
+</shape>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/input_view.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/input_view.xml
new file mode 100644
index 0000000..f229270
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/input_view.xml
@@ -0,0 +1,22 @@
+<?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.
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/input"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/qwerty_10_9_9.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/qwerty_10_9_9.xml
new file mode 100644
index 0000000..ee94ea9
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/layout/qwerty_10_9_9.xml
@@ -0,0 +1,200 @@
+<?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"
+    style="@style/KeyboardArea">
+
+    <View style="@style/KeyboardRow.Header"/>
+
+    <LinearLayout style="@style/KeyboardRow">
+        <TextView
+            android:id="@+id/key_pos_0_0"
+            android:text="q"
+            android:tag="KEYCODE_Q"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_0_1"
+            android:text="w"
+            android:tag="KEYCODE_W"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_0_2"
+            android:text="e"
+            android:tag="KEYCODE_E"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_0_3"
+            android:text="r"
+            android:tag="KEYCODE_R"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_0_4"
+            android:text="t"
+            android:tag="KEYCODE_T"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_0_5"
+            android:text="y"
+            android:tag="KEYCODE_Y"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_0_6"
+            android:text="u"
+            android:tag="KEYCODE_U"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_0_7"
+            android:text="i"
+            android:tag="KEYCODE_I"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_0_8"
+            android:text="o"
+            android:tag="KEYCODE_O"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_0_9"
+            android:text="p"
+            android:tag="KEYCODE_P"
+            style="@style/SoftKey"/>
+    </LinearLayout>
+
+    <LinearLayout style="@style/KeyboardRow">
+        <TextView
+            android:id="@+id/key_pos_1_0"
+            android:text="a"
+            android:tag="KEYCODE_A"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_1_1"
+            android:text="s"
+            android:tag="KEYCODE_S"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_1_2"
+            android:text="d"
+            android:tag="KEYCODE_D"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_1_3"
+            android:text="f"
+            android:tag="KEYCODE_F"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_1_4"
+            android:text="g"
+            android:tag="KEYCODE_G"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_1_5"
+            android:text="h"
+            android:tag="KEYCODE_H"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_1_6"
+            android:text="j"
+            android:tag="KEYCODE_J"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_1_7"
+            android:text="k"
+            android:tag="KEYCODE_K"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_1_8"
+            android:text="l"
+            android:tag="KEYCODE_L"
+            style="@style/SoftKey"/>
+    </LinearLayout>
+
+    <LinearLayout style="@style/KeyboardRow">
+        <TextView
+            android:id="@+id/key_pos_shift"
+            android:text="SHI"
+            android:tag="KEYCODE_SHIFT"
+            style="@style/SoftKey.Function"/>
+        <TextView
+            android:id="@+id/key_pos_2_0"
+            android:text="z"
+            android:tag="KEYCODE_Z"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_2_1"
+            android:text="x"
+            android:tag="KEYCODE_X"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_2_2"
+            style="@style/SoftKey"
+            android:text="c"
+            android:tag="KEYCODE_C"/>
+        <TextView
+            android:id="@+id/key_pos_2_3"
+            android:text="v"
+            android:tag="KEYCODE_V"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_2_4"
+            android:text="b"
+            android:tag="KEYCODE_B"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_2_5"
+            android:text="n"
+            android:tag="KEYCODE_N"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_2_6"
+            android:text="m"
+            android:tag="KEYCODE_M"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_del"
+            android:text="DEL"
+            android:tag="KEYCODE_DEL"
+            style="@style/SoftKey.Function"/>
+    </LinearLayout>
+
+    <LinearLayout style="@style/KeyboardRow">
+        <TextView
+            android:id="@+id/key_pos_symbol"
+            android:text="TAB"
+            android:tag="KEYCODE_TAB"
+            style="@style/SoftKey.Function"/>
+        <TextView
+            android:id="@+id/key_pos_comma"
+            android:text=","
+            android:tag="KEYCODE_COMMA"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_space"
+            android:text="SPACE"
+            android:tag="KEYCODE_SPACE"
+            style="@style/SoftKey.Space"/>
+        <TextView
+            android:id="@+id/key_pos_period"
+            android:text="."
+            android:tag="KEYCODE_PERIOD"
+            style="@style/SoftKey"/>
+        <TextView
+            android:id="@+id/key_pos_enter"
+            android:text="ENT"
+            android:tag="KEYCODE_ENTER"
+            style="@style/SoftKey.Function.Bottom"/>
+    </LinearLayout>
+</LinearLayout>
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/dimens.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/dimens.xml
new file mode 100644
index 0000000..1a4959e
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/dimens.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+
+<resources>
+    <dimen name="text_size_normal">24dp</dimen>
+    <dimen name="text_size_symbol">14dp</dimen>
+
+    <dimen name="keyboard_header_height">40dp</dimen>
+    <dimen name="keyboard_row_height">50dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/strings.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/strings.xml
new file mode 100644
index 0000000..11377fa
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?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.
+  -->
+
+<resources>
+    <string name="app_name">Fake IME</string>
+</resources>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/styles.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/styles.xml
new file mode 100644
index 0000000..83f7bc3
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/values/styles.xml
@@ -0,0 +1,66 @@
+<?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.
+  -->
+
+<resources>
+    <style name="KeyboardArea">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_gravity">bottom</item>
+        <item name="android:orientation">vertical</item>
+        <item name="android:background">#FFFFFFFF</item>
+    </style>
+
+    <style name="KeyboardRow">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">@dimen/keyboard_row_height</item>
+        <item name="android:orientation">horizontal</item>
+    </style>
+
+    <style name="KeyboardRow.Header">
+        <item name="android:layout_height">@dimen/keyboard_header_height</item>
+        <item name="android:background">#FFEEEEEE</item>
+    </style>
+
+    <style name="SoftKey">
+        <item name="android:layout_width">0dp</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:layout_weight">2</item>
+        <item name="android:gravity">center</item>
+        <item name="android:textColor">#FF000000</item>
+        <item name="android:textSize">@dimen/text_size_normal</item>
+        <item name="android:fontFamily">roboto-regular</item>
+        <item name="android:background">@drawable/key_border</item>
+    </style>
+
+    <style name="SoftKey.Function">
+        <item name="android:layout_weight">3</item>
+        <item name="android:textColor">#FF333333</item>
+        <item name="android:textSize">@dimen/text_size_symbol</item>
+    </style>
+
+    <style name="SoftKey.Function.Bottom">
+        <item name="android:layout_weight">3</item>
+        <item name="android:textColor">#FF333333</item>
+        <item name="android:textSize">@dimen/text_size_symbol</item>
+    </style>
+
+    <style name="SoftKey.Space">
+        <item name="android:layout_weight">10</item>
+        <item name="android:textColor">#FF333333</item>
+        <item name="android:textSize">@dimen/text_size_symbol</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml
new file mode 100644
index 0000000..872b068
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml
@@ -0,0 +1,23 @@
+<?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.
+  -->
+
+<input-method xmlns:android="http://schemas.android.com/apk/res/android">
+    <subtype
+        android:label="FakeIme"
+        android:imeSubtypeLocale="en_US"
+        android:imeSubtypeMode="keyboard"/>
+</input-method>
\ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/KeyCodeConstants.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/KeyCodeConstants.java
new file mode 100644
index 0000000..990fa24
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/KeyCodeConstants.java
@@ -0,0 +1,68 @@
+/*
+ * 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.apps.inputmethod.simpleime;
+
+import android.view.KeyEvent;
+
+import java.util.HashMap;
+
+/** Holder of key codes and their name. */
+public final class KeyCodeConstants {
+    private KeyCodeConstants() {}
+
+    static final HashMap<String, Integer> KEY_NAME_TO_CODE_MAP = new HashMap<>();
+
+    static {
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_A", KeyEvent.KEYCODE_A);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_B", KeyEvent.KEYCODE_B);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_C", KeyEvent.KEYCODE_C);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_D", KeyEvent.KEYCODE_D);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_E", KeyEvent.KEYCODE_E);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_F", KeyEvent.KEYCODE_F);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_G", KeyEvent.KEYCODE_G);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_H", KeyEvent.KEYCODE_H);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_I", KeyEvent.KEYCODE_I);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_J", KeyEvent.KEYCODE_J);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_K", KeyEvent.KEYCODE_K);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_L", KeyEvent.KEYCODE_L);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_M", KeyEvent.KEYCODE_M);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_N", KeyEvent.KEYCODE_N);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_O", KeyEvent.KEYCODE_O);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_P", KeyEvent.KEYCODE_P);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_Q", KeyEvent.KEYCODE_Q);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_R", KeyEvent.KEYCODE_R);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_S", KeyEvent.KEYCODE_S);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_T", KeyEvent.KEYCODE_T);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_U", KeyEvent.KEYCODE_U);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_V", KeyEvent.KEYCODE_V);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_W", KeyEvent.KEYCODE_W);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_X", KeyEvent.KEYCODE_X);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_Y", KeyEvent.KEYCODE_Y);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_Z", KeyEvent.KEYCODE_Z);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_SHIFT", KeyEvent.KEYCODE_SHIFT_LEFT);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_DEL", KeyEvent.KEYCODE_DEL);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_SPACE", KeyEvent.KEYCODE_SPACE);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_ENTER", KeyEvent.KEYCODE_ENTER);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_COMMA", KeyEvent.KEYCODE_COMMA);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_PERIOD", KeyEvent.KEYCODE_PERIOD);
+        KEY_NAME_TO_CODE_MAP.put("KEYCODE_TAB", KeyEvent.KEYCODE_TAB);
+    }
+
+    public static boolean isAlphaKeyCode(int keyCode) {
+        return keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z;
+    }
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleInputMethodService.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleInputMethodService.java
new file mode 100644
index 0000000..48942a3
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleInputMethodService.java
@@ -0,0 +1,67 @@
+/*
+ * 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.apps.inputmethod.simpleime;
+
+import android.inputmethodservice.InputMethodService;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.FrameLayout;
+
+import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper;
+
+/** The {@link InputMethodService} implementation for SimpeTestIme app. */
+public class SimpleInputMethodService extends InputMethodServiceWrapper {
+    private static final String TAG = "SimpleIMS";
+
+    private FrameLayout mInputView;
+
+    @Override
+    public View onCreateInputView() {
+        Log.i(TAG, "onCreateInputView()");
+        mInputView = (FrameLayout) LayoutInflater.from(this).inflate(R.layout.input_view, null);
+        return mInputView;
+    }
+
+    @Override
+    public void onStartInputView(EditorInfo info, boolean restarting) {
+        super.onStartInputView(info, restarting);
+        mInputView.removeAllViews();
+        SimpleKeyboard keyboard = new SimpleKeyboard(this, R.layout.qwerty_10_9_9);
+        mInputView.addView(keyboard.inflateKeyboardView(LayoutInflater.from(this), mInputView));
+    }
+
+    void handle(String data, int keyboardState) {
+        InputConnection inputConnection = getCurrentInputConnection();
+        Integer keyCode = KeyCodeConstants.KEY_NAME_TO_CODE_MAP.get(data);
+        Log.v(TAG, "keyCode: " + keyCode);
+        if (keyCode != null) {
+            inputConnection.sendKeyEvent(
+                    new KeyEvent(
+                            SystemClock.uptimeMillis(),
+                            SystemClock.uptimeMillis(),
+                            KeyEvent.ACTION_DOWN,
+                            keyCode,
+                            0,
+                            KeyCodeConstants.isAlphaKeyCode(keyCode) ? keyboardState : 0));
+        }
+    }
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboard.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboard.java
new file mode 100644
index 0000000..b16ec9eb
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboard.java
@@ -0,0 +1,126 @@
+/*
+ * 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.apps.inputmethod.simpleime;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/** Controls the visible virtual keyboard view. */
+final class SimpleKeyboard {
+    private static final String TAG = "SimpleKeyboard";
+
+    private static final int[] SOFT_KEY_IDS =
+            new int[] {
+                R.id.key_pos_0_0,
+                R.id.key_pos_0_1,
+                R.id.key_pos_0_2,
+                R.id.key_pos_0_3,
+                R.id.key_pos_0_4,
+                R.id.key_pos_0_5,
+                R.id.key_pos_0_6,
+                R.id.key_pos_0_7,
+                R.id.key_pos_0_8,
+                R.id.key_pos_0_9,
+                R.id.key_pos_1_0,
+                R.id.key_pos_1_1,
+                R.id.key_pos_1_2,
+                R.id.key_pos_1_3,
+                R.id.key_pos_1_4,
+                R.id.key_pos_1_5,
+                R.id.key_pos_1_6,
+                R.id.key_pos_1_7,
+                R.id.key_pos_1_8,
+                R.id.key_pos_2_0,
+                R.id.key_pos_2_1,
+                R.id.key_pos_2_2,
+                R.id.key_pos_2_3,
+                R.id.key_pos_2_4,
+                R.id.key_pos_2_5,
+                R.id.key_pos_2_6,
+                R.id.key_pos_shift,
+                R.id.key_pos_del,
+                R.id.key_pos_symbol,
+                R.id.key_pos_comma,
+                R.id.key_pos_space,
+                R.id.key_pos_period,
+                R.id.key_pos_enter,
+            };
+
+    private final SimpleInputMethodService mSimpleInputMethodService;
+    private final int mViewResId;
+    private final SparseArray<TextView> mSoftKeyViews = new SparseArray<>();
+    private View mKeyboardView;
+    private int mKeyboardState;
+
+    SimpleKeyboard(SimpleInputMethodService simpleInputMethodService, int viewResId) {
+        this.mSimpleInputMethodService = simpleInputMethodService;
+        this.mViewResId = viewResId;
+        this.mKeyboardState = 0;
+    }
+
+    View inflateKeyboardView(LayoutInflater inflater, ViewGroup inputView) {
+        mKeyboardView = inflater.inflate(mViewResId, inputView, false);
+        mapSoftKeys();
+        return mKeyboardView;
+    }
+
+    private void mapSoftKeys() {
+        for (int id : SOFT_KEY_IDS) {
+            TextView softKeyView = mKeyboardView.findViewById(id);
+            mSoftKeyViews.put(id, softKeyView);
+            String tagData = softKeyView.getTag() != null ? softKeyView.getTag().toString() : null;
+            softKeyView.setOnClickListener(v -> handle(tagData));
+        }
+    }
+
+    private void handle(String data) {
+        Log.i(TAG, "handle(): " + data);
+        if (TextUtils.isEmpty(data)) {
+            return;
+        }
+        if ("KEYCODE_SHIFT".equals(data)) {
+            handleShift();
+            return;
+        }
+
+        mSimpleInputMethodService.handle(data, mKeyboardState);
+    }
+
+    private void handleShift() {
+        mKeyboardState = toggleShiftState(mKeyboardState);
+        Log.v(TAG, "currentKeyboardState: " + mKeyboardState);
+        boolean isShiftOn = isShiftOn(mKeyboardState);
+        for (int i = 0; i < mSoftKeyViews.size(); i++) {
+            TextView softKeyView = mSoftKeyViews.valueAt(i);
+            softKeyView.setAllCaps(isShiftOn);
+        }
+    }
+
+    private static boolean isShiftOn(int state) {
+        return (state & KeyEvent.META_SHIFT_ON) == KeyEvent.META_SHIFT_ON;
+    }
+
+    private static int toggleShiftState(int state) {
+        return state ^ KeyEvent.META_SHIFT_ON;
+    }
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java
new file mode 100644
index 0000000..b706a65
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java
@@ -0,0 +1,102 @@
+/*
+ * 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.apps.inputmethod.simpleime.ims;
+
+import android.content.res.Configuration;
+import android.inputmethodservice.InputMethodService;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+
+import java.util.concurrent.CountDownLatch;
+
+/** Wrapper of {@link InputMethodService} to expose interfaces for testing purpose. */
+public class InputMethodServiceWrapper extends InputMethodService {
+    private static final String TAG = "InputMethodServiceWrapper";
+
+    private static InputMethodServiceWrapper sInputMethodServiceWrapper;
+
+    public static InputMethodServiceWrapper getInputMethodServiceWrapperForTesting() {
+        return sInputMethodServiceWrapper;
+    }
+
+    private boolean mInputViewStarted;
+    private CountDownLatch mCountDownLatchForTesting;
+
+    public boolean getCurrentInputViewStarted() {
+        return mInputViewStarted;
+    }
+
+    public void setCountDownLatchForTesting(CountDownLatch countDownLatchForTesting) {
+        mCountDownLatchForTesting = countDownLatchForTesting;
+    }
+
+    @Override
+    public void onCreate() {
+        Log.i(TAG, "onCreate()");
+        super.onCreate();
+        sInputMethodServiceWrapper = this;
+    }
+
+    @Override
+    public void onStartInput(EditorInfo info, boolean restarting) {
+        Log.i(TAG, "onStartInput() editor=" + info + ", restarting=" + restarting);
+        super.onStartInput(info, restarting);
+    }
+
+    @Override
+    public void onStartInputView(EditorInfo info, boolean restarting) {
+        Log.i(TAG, "onStartInputView() editor=" + info + ", restarting=" + restarting);
+        super.onStartInputView(info, restarting);
+        mInputViewStarted = true;
+        if (mCountDownLatchForTesting != null) {
+            mCountDownLatchForTesting.countDown();
+        }
+    }
+
+    @Override
+    public void onFinishInput() {
+        Log.i(TAG, "onFinishInput()");
+        super.onFinishInput();
+    }
+
+    @Override
+    public void onFinishInputView(boolean finishingInput) {
+        Log.i(TAG, "onFinishInputView()");
+        super.onFinishInputView(finishingInput);
+        mInputViewStarted = false;
+
+        if (mCountDownLatchForTesting != null) {
+            mCountDownLatchForTesting.countDown();
+        }
+    }
+
+    @Override
+    public void requestHideSelf(int flags) {
+        Log.i(TAG, "requestHideSelf() " + flags);
+        super.requestHideSelf(flags);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        Log.i(TAG, "onConfigurationChanged() " + newConfig);
+        super.onConfigurationChanged(newConfig);
+
+        if (mCountDownLatchForTesting != null) {
+            mCountDownLatchForTesting.countDown();
+        }
+    }
+}
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java
new file mode 100644
index 0000000..0eec7e6
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java
@@ -0,0 +1,105 @@
+/*
+ * 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.apps.inputmethod.simpleime.testing;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+/**
+ * A special activity for testing purpose.
+ *
+ * <p>This is used when the instruments package is SimpleTestIme, as the Intent needs to be started
+ * in the instruments package. More details see {@link
+ * Instrumentation#startActivitySync(Intent)}.</>
+ */
+public class TestActivity extends Activity {
+    private static final String TAG = "TestActivity";
+
+    /**
+     * Start a new test activity with an editor and wait for it to begin running before returning.
+     *
+     * @param instrumentation application instrumentation
+     * @return the newly started activity
+     */
+    public static TestActivity start(Instrumentation instrumentation) {
+        Intent intent =
+                new Intent()
+                        .setAction(Intent.ACTION_MAIN)
+                        .setClass(instrumentation.getTargetContext(), TestActivity.class)
+                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                        .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        return (TestActivity) instrumentation.startActivitySync(intent);
+    }
+
+    public EditText mEditText;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+        LinearLayout rootView = new LinearLayout(this);
+        mEditText = new EditText(this);
+        mEditText.setContentDescription("Input box");
+        rootView.addView(mEditText, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+        setContentView(rootView);
+        mEditText.requestFocus();
+        super.onCreate(savedInstanceState);
+    }
+
+    /** Shows soft keyboard via InputMethodManager. */
+    public boolean showImeWithInputMethodManager(int flags) {
+        InputMethodManager imm = getSystemService(InputMethodManager.class);
+        boolean result = imm.showSoftInput(mEditText, flags);
+        Log.i(TAG, "hideIme() via InputMethodManager, result=" + result);
+        return result;
+    }
+
+    /** Shows soft keyboard via WindowInsetsController. */
+    public boolean showImeWithWindowInsetsController() {
+        WindowInsetsController windowInsetsController = mEditText.getWindowInsetsController();
+        windowInsetsController.show(WindowInsets.Type.ime());
+        Log.i(TAG, "showIme() via WindowInsetsController");
+        return true;
+    }
+
+    /** Hides soft keyboard via InputMethodManager. */
+    public boolean hideImeWithInputMethodManager(int flags) {
+        InputMethodManager imm = getSystemService(InputMethodManager.class);
+        boolean result = imm.hideSoftInputFromWindow(mEditText.getWindowToken(), flags);
+        Log.i(TAG, "hideIme() via InputMethodManager, result=" + result);
+        return result;
+    }
+
+    /** Hides soft keyboard via WindowInsetsController. */
+    public boolean hideImeWithWindowInsetsController() {
+        WindowInsetsController windowInsetsController = mEditText.getWindowInsetsController();
+        windowInsetsController.hide(WindowInsets.Type.ime());
+        Log.i(TAG, "hideIme() via WindowInsetsController");
+        return true;
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index fc737d0..8e48490 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -34,6 +34,8 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
@@ -46,6 +48,7 @@
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.PermissionChecker;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.res.Resources;
@@ -63,6 +66,7 @@
 import com.android.server.LocalServices;
 import com.android.server.PowerAllowlistInternal;
 import com.android.server.SystemServiceManager;
+import com.android.server.job.controllers.ConnectivityController;
 import com.android.server.job.controllers.JobStatus;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.usage.AppStandbyInternal;
@@ -102,6 +106,7 @@
                 .initMocks(this)
                 .strictness(Strictness.LENIENT)
                 .mockStatic(LocalServices.class)
+                .mockStatic(PermissionChecker.class)
                 .mockStatic(ServiceManager.class)
                 .startMocking();
 
@@ -193,6 +198,15 @@
                 jobInfoBuilder.build(), callingUid, "com.android.test", 0, testTag);
     }
 
+    private void grantRunLongJobsPermission(boolean grant) {
+        final int permissionStatus = grant
+                ? PermissionChecker.PERMISSION_GRANTED : PermissionChecker.PERMISSION_HARD_DENIED;
+        doReturn(permissionStatus)
+                .when(() -> PermissionChecker.checkPermissionForPreflight(
+                        any(), eq(android.Manifest.permission.RUN_LONG_JOBS),
+                        anyInt(), anyInt(), anyString()));
+    }
+
     @Test
     public void testGetMinJobExecutionGuaranteeMs() {
         JobStatus ejMax = createJobStatus("testGetMinJobExecutionGuaranteeMs",
@@ -207,6 +221,15 @@
                 createJobInfo(5).setPriority(JobInfo.PRIORITY_HIGH));
         JobStatus jobDef = createJobStatus("testGetMinJobExecutionGuaranteeMs",
                 createJobInfo(6));
+        JobStatus jobDT = createJobStatus("testGetMinJobExecutionGuaranteeMs",
+                createJobInfo(7)
+                        .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+        JobStatus jobUI = createJobStatus("testGetMinJobExecutionGuaranteeMs",
+                createJobInfo(8)); // TODO(255371817): add setUserInitiated(true)
+        JobStatus jobUIDT = createJobStatus("testGetMinJobExecutionGuaranteeMs",
+                // TODO(255371817): add setUserInitiated(true)
+                createJobInfo(9)
+                        .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
 
         spyOn(ejMax);
         spyOn(ejHigh);
@@ -214,6 +237,9 @@
         spyOn(ejHighDowngraded);
         spyOn(jobHigh);
         spyOn(jobDef);
+        spyOn(jobDT);
+        spyOn(jobUI);
+        spyOn(jobUIDT);
 
         when(ejMax.shouldTreatAsExpeditedJob()).thenReturn(true);
         when(ejHigh.shouldTreatAsExpeditedJob()).thenReturn(true);
@@ -221,6 +247,16 @@
         when(ejHighDowngraded.shouldTreatAsExpeditedJob()).thenReturn(false);
         when(jobHigh.shouldTreatAsExpeditedJob()).thenReturn(false);
         when(jobDef.shouldTreatAsExpeditedJob()).thenReturn(false);
+        when(jobUI.shouldTreatAsUserInitiated()).thenReturn(true);
+        when(jobUIDT.shouldTreatAsUserInitiated()).thenReturn(true);
+
+        ConnectivityController connectivityController = mService.getConnectivityController();
+        spyOn(connectivityController);
+        mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS;
+        mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS = 60 * MINUTE_IN_MILLIS;
+        mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.5f;
+        mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = HOUR_IN_MILLIS;
+        mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = 6 * HOUR_IN_MILLIS;
 
         assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS,
                 mService.getMinJobExecutionGuaranteeMs(ejMax));
@@ -234,8 +270,81 @@
                 mService.getMinJobExecutionGuaranteeMs(jobHigh));
         assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS,
                 mService.getMinJobExecutionGuaranteeMs(jobDef));
+        grantRunLongJobsPermission(false); // Without permission
+        assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS,
+                mService.getMinJobExecutionGuaranteeMs(jobDT));
+        grantRunLongJobsPermission(true); // With permission
+        doReturn(ConnectivityController.UNKNOWN_TIME)
+                .when(connectivityController).getEstimatedTransferTimeMs(any());
+        assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+                mService.getMinJobExecutionGuaranteeMs(jobDT));
+        doReturn(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS / 2)
+                .when(connectivityController).getEstimatedTransferTimeMs(any());
+        assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+                mService.getMinJobExecutionGuaranteeMs(jobDT));
+        doReturn(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS * 2)
+                .when(connectivityController).getEstimatedTransferTimeMs(any());
+        assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+                mService.getMinJobExecutionGuaranteeMs(jobDT));
+        doReturn(mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS * 2)
+                .when(connectivityController).getEstimatedTransferTimeMs(any());
+        assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS,
+                mService.getMinJobExecutionGuaranteeMs(jobDT));
+        // UserInitiated
+        assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+                mService.getMinJobExecutionGuaranteeMs(jobUI));
+        grantRunLongJobsPermission(false);
+        assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS,
+                mService.getMinJobExecutionGuaranteeMs(jobUIDT));
+        grantRunLongJobsPermission(true); // With permission
+        doReturn(ConnectivityController.UNKNOWN_TIME)
+                .when(connectivityController).getEstimatedTransferTimeMs(any());
+        assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+                mService.getMinJobExecutionGuaranteeMs(jobUIDT));
+        doReturn(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS / 2)
+                .when(connectivityController).getEstimatedTransferTimeMs(any());
+        assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS,
+                mService.getMinJobExecutionGuaranteeMs(jobUIDT));
+        doReturn(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS * 2)
+                .when(connectivityController).getEstimatedTransferTimeMs(any());
+        assertEquals(
+                (long) (mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS
+                        * 2 * 1.5),
+                mService.getMinJobExecutionGuaranteeMs(jobUIDT));
+        doReturn(mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS * 2)
+                .when(connectivityController).getEstimatedTransferTimeMs(any());
+        assertEquals(mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS,
+                mService.getMinJobExecutionGuaranteeMs(jobUIDT));
     }
 
+    @Test
+    public void testGetMaxJobExecutionTimeMs() {
+        JobStatus jobDT = createJobStatus("testGetMaxJobExecutionTimeMs",
+                createJobInfo(7)
+                        .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+        JobStatus jobUI = createJobStatus("testGetMaxJobExecutionTimeMs",
+                createJobInfo(9)); // TODO(255371817): add setUserInitiated(true)
+        JobStatus jobUIDT = createJobStatus("testGetMaxJobExecutionTimeMs",
+                // TODO(255371817): add setUserInitiated(true)
+                createJobInfo(10)
+                        .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+
+        spyOn(jobDT);
+        spyOn(jobUI);
+        spyOn(jobUIDT);
+
+        when(jobUI.shouldTreatAsUserInitiated()).thenReturn(true);
+        when(jobUIDT.shouldTreatAsUserInitiated()).thenReturn(true);
+
+        grantRunLongJobsPermission(true);
+
+        assertEquals(mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS,
+                mService.getMaxJobExecutionTimeMs(jobDT));
+        assertEquals(mService.mConstants.RUNTIME_USER_INITIATED_LIMIT_MS,
+                mService.getMaxJobExecutionTimeMs(jobUI));
+        assertEquals(mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS,
+                mService.getMaxJobExecutionTimeMs(jobUIDT));
+    }
 
     /**
      * Confirm that {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int)}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
index 1f85f2c..42e22f3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -24,6 +24,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -35,6 +36,7 @@
 import static com.android.server.job.JobSchedulerService.RARE_INDEX;
 import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -240,20 +242,20 @@
                         .setLinkDownstreamBandwidthKbps(1).build(), mConstants));
         // Slow downstream
         assertFalse(controller.isSatisfied(createJobStatus(job), net,
-                createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(137)
+                createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(140)
                         .setLinkDownstreamBandwidthKbps(1).build(), mConstants));
         // Slow upstream
         assertFalse(controller.isSatisfied(createJobStatus(job), net,
                 createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1)
-                        .setLinkDownstreamBandwidthKbps(137).build(), mConstants));
+                        .setLinkDownstreamBandwidthKbps(140).build(), mConstants));
         // Network good enough
         assertTrue(controller.isSatisfied(createJobStatus(job), net,
-                createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(137)
-                        .setLinkDownstreamBandwidthKbps(137).build(), mConstants));
+                createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(140)
+                        .setLinkDownstreamBandwidthKbps(140).build(), mConstants));
         // Network slightly too slow given reduced time
         assertFalse(controller.isSatisfied(createJobStatus(job), net,
-                createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(130)
-                        .setLinkDownstreamBandwidthKbps(130).build(), mConstants));
+                createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(139)
+                        .setLinkDownstreamBandwidthKbps(139).build(), mConstants));
         // Slow network is too slow, but device is charging and network is unmetered.
         when(mService.isBatteryCharging()).thenReturn(true);
         controller.onBatteryStateChangedLocked();
@@ -1188,6 +1190,78 @@
         assertFalse(unnetworked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
     }
 
+    @Test
+    public void testCalculateTransferTimeMs() {
+        assertEquals(ConnectivityController.UNKNOWN_TIME,
+                ConnectivityController.calculateTransferTimeMs(1, 0));
+        assertEquals(ConnectivityController.UNKNOWN_TIME,
+                ConnectivityController.calculateTransferTimeMs(JobInfo.NETWORK_BYTES_UNKNOWN, 512));
+        assertEquals(1, ConnectivityController.calculateTransferTimeMs(1, 8));
+        assertEquals(1000, ConnectivityController.calculateTransferTimeMs(1000, 8));
+        assertEquals(8, ConnectivityController.calculateTransferTimeMs(1024, 1024));
+    }
+
+    @Test
+    public void testGetEstimatedTransferTimeMs() {
+        final ArgumentCaptor<NetworkCallback> callbackCaptor =
+                ArgumentCaptor.forClass(NetworkCallback.class);
+        doNothing().when(mConnManager).registerNetworkCallback(any(), callbackCaptor.capture());
+
+        final JobStatus job = createJobStatus(createJob()
+                .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(10_000),
+                        DataUnit.MEBIBYTES.toBytes(1_000))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
+
+        final ConnectivityController controller = new ConnectivityController(mService,
+                mFlexibilityController);
+
+        final JobStatus jobNoEstimates = createJobStatus(createJob());
+        assertEquals(ConnectivityController.UNKNOWN_TIME,
+                controller.getEstimatedTransferTimeMs(jobNoEstimates));
+
+        // No network
+        job.network = null;
+        assertEquals(ConnectivityController.UNKNOWN_TIME,
+                controller.getEstimatedTransferTimeMs(job));
+
+        final NetworkCallback generalCallback = callbackCaptor.getValue();
+
+        // No capabilities
+        final Network network = mock(Network.class);
+        answerNetwork(generalCallback, null, null, network, null);
+        job.network = network;
+        assertEquals(ConnectivityController.UNKNOWN_TIME,
+                controller.getEstimatedTransferTimeMs(job));
+
+        // Capabilities don't have bandwidth values
+        NetworkCapabilities caps = createCapabilitiesBuilder().build();
+        answerNetwork(generalCallback, null, null, network, caps);
+        assertEquals(ConnectivityController.UNKNOWN_TIME,
+                controller.getEstimatedTransferTimeMs(job));
+
+        // Capabilities only has downstream bandwidth
+        caps = createCapabilitiesBuilder()
+                .setLinkDownstreamBandwidthKbps(1024)
+                .build();
+        answerNetwork(generalCallback, null, null, network, caps);
+        assertEquals(81920 * SECOND_IN_MILLIS, controller.getEstimatedTransferTimeMs(job));
+
+        // Capabilities only has upstream bandwidth
+        caps = createCapabilitiesBuilder()
+                .setLinkUpstreamBandwidthKbps(2 * 1024)
+                .build();
+        answerNetwork(generalCallback, null, null, network, caps);
+        assertEquals(4096 * SECOND_IN_MILLIS, controller.getEstimatedTransferTimeMs(job));
+
+        // Capabilities only both stream bandwidths
+        caps = createCapabilitiesBuilder()
+                .setLinkDownstreamBandwidthKbps(1024)
+                .setLinkUpstreamBandwidthKbps(2 * 1024)
+                .build();
+        answerNetwork(generalCallback, null, null, network, caps);
+        assertEquals((81920 + 4096) * SECOND_IN_MILLIS, controller.getEstimatedTransferTimeMs(job));
+    }
+
     private void answerNetwork(@NonNull NetworkCallback generalCallback,
             @Nullable NetworkCallback uidCallback, @Nullable Network lastNetwork,
             @Nullable Network net, @Nullable NetworkCapabilities caps) {
@@ -1198,11 +1272,15 @@
             }
         } else {
             generalCallback.onAvailable(net);
-            generalCallback.onCapabilitiesChanged(net, caps);
+            if (caps != null) {
+                generalCallback.onCapabilitiesChanged(net, caps);
+            }
             if (uidCallback != null) {
                 uidCallback.onAvailable(net);
                 uidCallback.onBlockedStatusChanged(net, ConnectivityManager.BLOCKED_REASON_NONE);
-                uidCallback.onCapabilitiesChanged(net, caps);
+                if (caps != null) {
+                    uidCallback.onCapabilitiesChanged(net, caps);
+                }
             }
         }
     }
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyRemapperTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyRemapperTests.kt
new file mode 100644
index 0000000..c22782c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/input/KeyRemapperTests.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.input
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.hardware.input.IInputManager
+import android.hardware.input.InputManager
+import android.os.test.TestLooper
+import android.platform.test.annotations.Presubmit
+import android.view.InputDevice
+import android.view.KeyEvent
+import androidx.test.core.app.ApplicationProvider
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+
+private fun createKeyboard(deviceId: Int): InputDevice =
+    InputDevice.Builder()
+        .setId(deviceId)
+        .setName("Device $deviceId")
+        .setDescriptor("descriptor $deviceId")
+        .setSources(InputDevice.SOURCE_KEYBOARD)
+        .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+        .setExternal(true)
+        .build()
+
+/**
+ * Tests for {@link KeyRemapper}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:KeyRemapperTests
+ */
+@Presubmit
+class KeyRemapperTests {
+
+    companion object {
+        const val DEVICE_ID = 1
+        val REMAPPABLE_KEYS = intArrayOf(
+            KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT,
+            KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_META_RIGHT,
+            KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT,
+            KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT,
+            KeyEvent.KEYCODE_CAPS_LOCK
+        )
+    }
+
+    @get:Rule
+    val rule = MockitoJUnit.rule()!!
+
+    @Mock
+    private lateinit var iInputManager: IInputManager
+    @Mock
+    private lateinit var native: NativeInputManagerService
+    private lateinit var mKeyRemapper: KeyRemapper
+    private lateinit var context: Context
+    private lateinit var dataStore: PersistentDataStore
+    private lateinit var testLooper: TestLooper
+
+    @Before
+    fun setup() {
+        context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+        dataStore = PersistentDataStore(object : PersistentDataStore.Injector() {
+            override fun openRead(): InputStream? {
+                throw FileNotFoundException()
+            }
+
+            override fun startWrite(): FileOutputStream? {
+                throw IOException()
+            }
+
+            override fun finishWrite(fos: FileOutputStream?, success: Boolean) {}
+        })
+        testLooper = TestLooper()
+        mKeyRemapper = KeyRemapper(
+            context,
+            native,
+            dataStore,
+            testLooper.looper
+        )
+        val inputManager = InputManager.resetInstance(iInputManager)
+        Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
+            .thenReturn(inputManager)
+        Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
+    }
+
+    @After
+    fun tearDown() {
+        InputManager.clearInstance()
+    }
+
+    @Test
+    fun testKeyRemapping() {
+        val keyboard = createKeyboard(DEVICE_ID)
+        Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard)
+
+        for (i in REMAPPABLE_KEYS.indices) {
+            val fromKeyCode = REMAPPABLE_KEYS[i]
+            val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size]
+            mKeyRemapper.remapKey(fromKeyCode, toKeyCode)
+            testLooper.dispatchNext()
+        }
+
+        val remapping = mKeyRemapper.keyRemapping
+        val expectedSize = REMAPPABLE_KEYS.size
+        assertEquals("Remapping size should be $expectedSize", expectedSize, remapping.size)
+
+        for (i in REMAPPABLE_KEYS.indices) {
+            val fromKeyCode = REMAPPABLE_KEYS[i]
+            val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size]
+            assertEquals(
+                "Remapping should include mapping from $fromKeyCode to $toKeyCode",
+                toKeyCode,
+                remapping.getOrDefault(fromKeyCode, -1)
+            )
+        }
+
+        mKeyRemapper.clearAllKeyRemappings()
+        testLooper.dispatchNext()
+
+        assertEquals(
+            "Remapping size should be 0 after clearAllModifierKeyRemappings",
+            0,
+            mKeyRemapper.keyRemapping.size
+        )
+    }
+}
\ No newline at end of file