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