Merge changes I1eafd69c,I4c5e401c into main
* changes:
Skip performance hint for the end of scene transition
Fix missing transition with visibility change when changing opaque
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index ce311d0..c43aa75 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -74,6 +74,8 @@
":android.chre.flags-aconfig-java{.generated_srcjars}",
":android.speech.flags-aconfig-java{.generated_srcjars}",
":power_flags_lib{.generated_srcjars}",
+ ":android.content.flags-aconfig-java{.generated_srcjars}",
+ ":aconfig_mediacodec_flags_java_lib{.generated_srcjars}",
]
filegroup {
@@ -455,6 +457,13 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+java_aconfig_library {
+ name: "com.android.media.flags.bettertogether-aconfig-java-host",
+ aconfig_declarations: "com.android.media.flags.bettertogether-aconfig",
+ host_supported: true,
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// Media TV
aconfig_declarations {
name: "android.media.tv.flags-aconfig",
@@ -940,3 +949,16 @@
aconfig_declarations: "power_flags",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+
+// Content
+aconfig_declarations {
+ name: "android.content.flags-aconfig",
+ package: "android.content.flags",
+ srcs: ["core/java/android/content/flags/flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "android.content.flags-aconfig-java",
+ aconfig_declarations: "android.content.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/Android.bp b/Android.bp
index a3e39018..485f1be 100644
--- a/Android.bp
+++ b/Android.bp
@@ -149,6 +149,7 @@
":framework-javastream-protos",
":statslog-framework-java-gen", // FrameworkStatsLog.java
":audio_policy_configuration_V7_0",
+ ":perfetto_trace_javastream_protos",
],
}
diff --git a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java
index caf7e7f..1fc888b 100644
--- a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java
@@ -16,6 +16,7 @@
package com.android.server;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.PowerExemptionManager;
import android.os.PowerExemptionManager.ReasonCode;
@@ -77,6 +78,9 @@
int[] getPowerSaveTempWhitelistAppIds();
+ @NonNull
+ String[] getFullPowerWhitelistExceptIdle();
+
/**
* Listener to be notified when DeviceIdleController determines that the device has moved or is
* stationary.
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index 6383ed8..31214cb 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -2374,6 +2374,11 @@
return DeviceIdleController.this.isAppOnWhitelistInternal(appid);
}
+ @Override
+ public String[] getFullPowerWhitelistExceptIdle() {
+ return DeviceIdleController.this.getFullPowerWhitelistInternalUnchecked();
+ }
+
/**
* Returns the array of app ids whitelisted by user. Take care not to
* modify this, as it is a reference to the original copy. But the reference
@@ -3100,10 +3105,14 @@
}
private String[] getFullPowerWhitelistInternal(final int callingUid, final int callingUserId) {
- final String[] apps;
+ return ArrayUtils.filter(getFullPowerWhitelistInternalUnchecked(), String[]::new,
+ (pkg) -> !mPackageManagerInternal.filterAppAccess(pkg, callingUid, callingUserId));
+ }
+
+ private String[] getFullPowerWhitelistInternalUnchecked() {
synchronized (this) {
int size = mPowerSaveWhitelistApps.size() + mPowerSaveWhitelistUserApps.size();
- apps = new String[size];
+ final String[] apps = new String[size];
int cur = 0;
for (int i = 0; i < mPowerSaveWhitelistApps.size(); i++) {
apps[cur] = mPowerSaveWhitelistApps.keyAt(i);
@@ -3113,9 +3122,8 @@
apps[cur] = mPowerSaveWhitelistUserApps.keyAt(i);
cur++;
}
+ return apps;
}
- return ArrayUtils.filter(apps, String[]::new,
- (pkg) -> !mPackageManagerInternal.filterAppAccess(pkg, callingUid, callingUserId));
}
public boolean isPowerSaveWhitelistExceptIdleAppInternal(String packageName) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
index e3ba50d..e3ac780 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
@@ -318,6 +318,10 @@
try {
final boolean isStopped = mPackageManagerInternal.isPackageStopped(packageName, uid);
+ if (DEBUG) {
+ Slog.d(TAG,
+ "Pulled stopped state of " + packageName + " (" + uid + "): " + isStopped);
+ }
mPackageStoppedState.add(uid, packageName, isStopped);
return isStopped;
} catch (PackageManager.NameNotFoundException e) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index 0e67b9a..2c9af67 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -30,11 +30,15 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.job.JobInfo;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.PowerManager;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.util.ArraySet;
@@ -48,6 +52,8 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.AppSchedulingModuleThread;
+import com.android.server.DeviceIdleInternal;
+import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.utils.AlarmQueue;
@@ -127,6 +133,19 @@
@GuardedBy("mLock")
private final SparseLongArray mLastSeenConstraintTimesElapsed = new SparseLongArray();
+ private DeviceIdleInternal mDeviceIdleInternal;
+ private final ArraySet<String> mPowerAllowlistedApps = new ArraySet<>();
+
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
+ mHandler.post(FlexibilityController.this::updatePowerAllowlistCache);
+ break;
+ }
+ }
+ };
@VisibleForTesting
@GuardedBy("mLock")
final FlexibilityTracker mFlexibilityTracker;
@@ -180,8 +199,16 @@
}
};
- private static final int MSG_UPDATE_JOBS = 0;
- private static final int MSG_UPDATE_JOB = 1;
+ private static final int MSG_CHECK_ALL_JOBS = 0;
+ /** Check the jobs in {@link #mJobsToCheck} */
+ private static final int MSG_CHECK_JOBS = 1;
+ /** Check the jobs of packages in {@link #mPackagesToCheck} */
+ private static final int MSG_CHECK_PACKAGES = 2;
+
+ @GuardedBy("mLock")
+ private final ArraySet<JobStatus> mJobsToCheck = new ArraySet<>();
+ @GuardedBy("mLock")
+ private final ArraySet<String> mPackagesToCheck = new ArraySet<>();
public FlexibilityController(
JobSchedulerService service, PrefetchController prefetchController) {
@@ -204,6 +231,16 @@
mPercentToDropConstraints =
mFcConfig.DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS;
mPrefetchController = prefetchController;
+
+ if (mFlexibilityEnabled) {
+ registerBroadcastReceiver();
+ }
+ }
+
+ @Override
+ public void onSystemServicesReady() {
+ mDeviceIdleInternal = LocalServices.getService(DeviceIdleInternal.class);
+ mHandler.post(FlexibilityController.this::updatePowerAllowlistCache);
}
@Override
@@ -241,6 +278,7 @@
mFlexibilityAlarmQueue.removeAlarmForKey(js);
mFlexibilityTracker.remove(js);
}
+ mJobsToCheck.remove(js);
}
@Override
@@ -266,7 +304,14 @@
@GuardedBy("mLock")
boolean isFlexibilitySatisfiedLocked(JobStatus js) {
return !mFlexibilityEnabled
+ // Exclude all jobs of the TOP app
|| mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP
+ // Only exclude DEFAULT+ priority jobs for BFGS+ apps
+ || (mService.getUidBias(js.getSourceUid()) >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
+ && js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT)
+ // For apps in the power allowlist, automatically exclude DEFAULT+ priority jobs.
+ || (js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT
+ && mPowerAllowlistedApps.contains(js.getSourcePackageName()))
|| hasEnoughSatisfiedConstraintsLocked(js)
|| mService.isCurrentlyRunningLocked(js);
}
@@ -371,7 +416,7 @@
// Push the job update to the handler to avoid blocking other controllers and
// potentially batch back-to-back controller state updates together.
- mHandler.obtainMessage(MSG_UPDATE_JOBS).sendToTarget();
+ mHandler.obtainMessage(MSG_CHECK_ALL_JOBS).sendToTarget();
}
}
}
@@ -485,7 +530,9 @@
@Override
@GuardedBy("mLock")
public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) {
- if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) {
+ if (prevBias < JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
+ && newBias < JobInfo.BIAS_BOUND_FOREGROUND_SERVICE) {
+ // All changes are below BFGS. There's no significant change to care about.
return;
}
final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -557,6 +604,39 @@
mFcConfig.processConstantLocked(properties, key);
}
+ private void registerBroadcastReceiver() {
+ IntentFilter filter = new IntentFilter(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+ }
+
+ private void unregisterBroadcastReceiver() {
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ }
+
+ private void updatePowerAllowlistCache() {
+ if (mDeviceIdleInternal == null) {
+ return;
+ }
+
+ // Don't call out to DeviceIdleController with the lock held.
+ final String[] allowlistedPkgs = mDeviceIdleInternal.getFullPowerWhitelistExceptIdle();
+ final ArraySet<String> changedPkgs = new ArraySet<>();
+ synchronized (mLock) {
+ changedPkgs.addAll(mPowerAllowlistedApps);
+ mPowerAllowlistedApps.clear();
+ for (final String pkgName : allowlistedPkgs) {
+ mPowerAllowlistedApps.add(pkgName);
+ if (changedPkgs.contains(pkgName)) {
+ changedPkgs.remove(pkgName);
+ } else {
+ changedPkgs.add(pkgName);
+ }
+ }
+ mPackagesToCheck.addAll(changedPkgs);
+ mHandler.sendEmptyMessage(MSG_CHECK_PACKAGES);
+ }
+ }
+
@VisibleForTesting
class FlexibilityTracker {
final ArrayList<ArraySet<JobStatus>> mTrackedJobs;
@@ -710,7 +790,8 @@
}
mFlexibilityTracker.setNumDroppedFlexibleConstraints(js,
js.getNumAppliedFlexibleConstraints());
- mHandler.obtainMessage(MSG_UPDATE_JOB, js).sendToTarget();
+ mJobsToCheck.add(js);
+ mHandler.sendEmptyMessage(MSG_CHECK_JOBS);
return;
}
if (nextTimeElapsed == NO_LIFECYCLE_END) {
@@ -761,10 +842,12 @@
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
- case MSG_UPDATE_JOBS:
- removeMessages(MSG_UPDATE_JOBS);
+ case MSG_CHECK_ALL_JOBS:
+ removeMessages(MSG_CHECK_ALL_JOBS);
synchronized (mLock) {
+ mJobsToCheck.clear();
+ mPackagesToCheck.clear();
final long nowElapsed = sElapsedRealtimeClock.millis();
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
@@ -790,19 +873,50 @@
}
break;
- case MSG_UPDATE_JOB:
+ case MSG_CHECK_JOBS:
synchronized (mLock) {
- final JobStatus js = (JobStatus) msg.obj;
- if (DEBUG) {
- Slog.d("blah", "Checking on " + js.toShortString());
- }
final long nowElapsed = sElapsedRealtimeClock.millis();
- if (js.setFlexibilityConstraintSatisfied(
- nowElapsed, isFlexibilitySatisfiedLocked(js))) {
- // TODO(141645789): add method that will take a single job
- ArraySet<JobStatus> changedJob = new ArraySet<>();
- changedJob.add(js);
- mStateChangedListener.onControllerStateChanged(changedJob);
+ ArraySet<JobStatus> changedJobs = new ArraySet<>();
+
+ for (int i = mJobsToCheck.size() - 1; i >= 0; --i) {
+ final JobStatus js = mJobsToCheck.valueAt(i);
+ if (DEBUG) {
+ Slog.d(TAG, "Checking on " + js.toShortString());
+ }
+ if (js.setFlexibilityConstraintSatisfied(
+ nowElapsed, isFlexibilitySatisfiedLocked(js))) {
+ changedJobs.add(js);
+ }
+ }
+
+ mJobsToCheck.clear();
+ if (changedJobs.size() > 0) {
+ mStateChangedListener.onControllerStateChanged(changedJobs);
+ }
+ }
+ break;
+
+ case MSG_CHECK_PACKAGES:
+ synchronized (mLock) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final ArraySet<JobStatus> changedJobs = new ArraySet<>();
+
+ mService.getJobStore().forEachJob(
+ (js) -> mPackagesToCheck.contains(js.getSourcePackageName())
+ || mPackagesToCheck.contains(js.getCallingPackageName()),
+ (js) -> {
+ if (DEBUG) {
+ Slog.d(TAG, "Checking on " + js.toShortString());
+ }
+ if (js.setFlexibilityConstraintSatisfied(
+ nowElapsed, isFlexibilitySatisfiedLocked(js))) {
+ changedJobs.add(js);
+ }
+ });
+
+ mPackagesToCheck.clear();
+ if (changedJobs.size() > 0) {
+ mStateChangedListener.onControllerStateChanged(changedJobs);
}
}
break;
@@ -882,10 +996,12 @@
mFlexibilityEnabled = true;
mPrefetchController
.registerPrefetchChangedListener(mPrefetchChangedListener);
+ registerBroadcastReceiver();
} else {
mFlexibilityEnabled = false;
mPrefetchController
.unRegisterPrefetchChangedListener(mPrefetchChangedListener);
+ unregisterBroadcastReceiver();
}
}
break;
@@ -985,7 +1101,14 @@
pw.println(":");
pw.increaseIndent();
- pw.print(KEY_APPLIED_CONSTRAINTS, APPLIED_CONSTRAINTS).println();
+ pw.print(KEY_APPLIED_CONSTRAINTS, APPLIED_CONSTRAINTS);
+ pw.print("(");
+ if (APPLIED_CONSTRAINTS != 0) {
+ JobStatus.dumpConstraints(pw, APPLIED_CONSTRAINTS);
+ } else {
+ pw.print("nothing");
+ }
+ pw.println(")");
pw.print(KEY_DEADLINE_PROXIMITY_LIMIT, DEADLINE_PROXIMITY_LIMIT_MS).println();
pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE, FALLBACK_FLEXIBILITY_DEADLINE_MS).println();
pw.print(KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS,
@@ -1044,6 +1167,10 @@
pw.decreaseIndent();
pw.println();
+ pw.print("Power allowlisted packages: ");
+ pw.println(mPowerAllowlistedApps);
+
+ pw.println();
mFlexibilityTracker.dump(pw, predicate);
pw.println();
mFlexibilityAlarmQueue.dump(pw);
diff --git a/boot/Android.bp b/boot/Android.bp
index 8a3d35e..c4a1139b7 100644
--- a/boot/Android.bp
+++ b/boot/Android.bp
@@ -26,9 +26,10 @@
soong_config_module_type {
name: "custom_platform_bootclasspath",
module_type: "platform_bootclasspath",
- config_namespace: "AUTO",
+ config_namespace: "bootclasspath",
bool_variables: [
"car_bootclasspath_fragment",
+ "nfc_apex_bootclasspath_fragment",
],
properties: [
"fragments",
@@ -155,6 +156,15 @@
},
],
},
+ nfc_apex_bootclasspath_fragment: {
+ fragments: [
+ // only used if NFC mainline is enabled.
+ {
+ apex: "com.android.nfcservices",
+ module: "com.android.nfcservices-bootclasspath-fragment",
+ },
+ ],
+ },
},
// Additional information needed by hidden api processing.
diff --git a/boot/hiddenapi/hiddenapi-max-target-o.txt b/boot/hiddenapi/hiddenapi-max-target-o.txt
index 3c16915..2d417ce 100644
--- a/boot/hiddenapi/hiddenapi-max-target-o.txt
+++ b/boot/hiddenapi/hiddenapi-max-target-o.txt
@@ -33834,649 +33834,6 @@
Landroid/net/WifiLinkQualityInfo;->setTxBad(J)V
Landroid/net/WifiLinkQualityInfo;->setTxGood(J)V
Landroid/net/WifiLinkQualityInfo;->setType(I)V
-Landroid/nfc/ApduList;-><init>()V
-Landroid/nfc/ApduList;-><init>(Landroid/os/Parcel;)V
-Landroid/nfc/ApduList;->add([B)V
-Landroid/nfc/ApduList;->commands:Ljava/util/ArrayList;
-Landroid/nfc/ApduList;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/nfc/ApduList;->get()Ljava/util/List;
-Landroid/nfc/BeamShareData;-><init>(Landroid/nfc/NdefMessage;[Landroid/net/Uri;Landroid/os/UserHandle;I)V
-Landroid/nfc/BeamShareData;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/nfc/BeamShareData;->flags:I
-Landroid/nfc/BeamShareData;->ndefMessage:Landroid/nfc/NdefMessage;
-Landroid/nfc/BeamShareData;->uris:[Landroid/net/Uri;
-Landroid/nfc/BeamShareData;->userHandle:Landroid/os/UserHandle;
-Landroid/nfc/cardemulation/AidGroup;-><init>(Ljava/util/List;Ljava/lang/String;)V
-Landroid/nfc/cardemulation/AidGroup;->isValidCategory(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/AidGroup;->MAX_NUM_AIDS:I
-Landroid/nfc/cardemulation/AidGroup;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/ApduServiceInfo;->dump(Ljava/io/FileDescriptor;Ljava/io/PrintWriter;[Ljava/lang/String;)V
-Landroid/nfc/cardemulation/ApduServiceInfo;->getAidGroups()Ljava/util/ArrayList;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getAids()Ljava/util/List;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getCategoryForAid(Ljava/lang/String;)Ljava/lang/String;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getComponent()Landroid/content/ComponentName;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getDynamicAidGroupForCategory(Ljava/lang/String;)Landroid/nfc/cardemulation/AidGroup;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getPrefixAids()Ljava/util/List;
-Landroid/nfc/cardemulation/ApduServiceInfo;->getSubsetAids()Ljava/util/List;
-Landroid/nfc/cardemulation/ApduServiceInfo;->hasCategory(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/ApduServiceInfo;->loadAppLabel(Landroid/content/pm/PackageManager;)Ljava/lang/CharSequence;
-Landroid/nfc/cardemulation/ApduServiceInfo;->loadIcon(Landroid/content/pm/PackageManager;)Landroid/graphics/drawable/Drawable;
-Landroid/nfc/cardemulation/ApduServiceInfo;->loadLabel(Landroid/content/pm/PackageManager;)Ljava/lang/CharSequence;
-Landroid/nfc/cardemulation/ApduServiceInfo;->mBannerResourceId:I
-Landroid/nfc/cardemulation/ApduServiceInfo;->mDescription:Ljava/lang/String;
-Landroid/nfc/cardemulation/ApduServiceInfo;->mOnHost:Z
-Landroid/nfc/cardemulation/ApduServiceInfo;->mRequiresDeviceUnlock:Z
-Landroid/nfc/cardemulation/ApduServiceInfo;->mSettingsActivityName:Ljava/lang/String;
-Landroid/nfc/cardemulation/ApduServiceInfo;->mUid:I
-Landroid/nfc/cardemulation/ApduServiceInfo;->removeDynamicAidGroupForCategory(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/ApduServiceInfo;->setOrReplaceDynamicAidGroup(Landroid/nfc/cardemulation/AidGroup;)V
-Landroid/nfc/cardemulation/ApduServiceInfo;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/CardEmulation;-><init>(Landroid/content/Context;Landroid/nfc/INfcCardEmulation;)V
-Landroid/nfc/cardemulation/CardEmulation;->getServices(Ljava/lang/String;)Ljava/util/List;
-Landroid/nfc/cardemulation/CardEmulation;->isValidAid(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/CardEmulation;->mContext:Landroid/content/Context;
-Landroid/nfc/cardemulation/CardEmulation;->recoverService()V
-Landroid/nfc/cardemulation/CardEmulation;->sCardEmus:Ljava/util/HashMap;
-Landroid/nfc/cardemulation/CardEmulation;->setDefaultForNextTap(Landroid/content/ComponentName;)Z
-Landroid/nfc/cardemulation/CardEmulation;->setDefaultServiceForCategory(Landroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/CardEmulation;->sIsInitialized:Z
-Landroid/nfc/cardemulation/CardEmulation;->sService:Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/cardemulation/CardEmulation;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/HostApduService;->KEY_DATA:Ljava/lang/String;
-Landroid/nfc/cardemulation/HostApduService;->mMessenger:Landroid/os/Messenger;
-Landroid/nfc/cardemulation/HostApduService;->mNfcService:Landroid/os/Messenger;
-Landroid/nfc/cardemulation/HostApduService;->MSG_COMMAND_APDU:I
-Landroid/nfc/cardemulation/HostApduService;->MSG_DEACTIVATED:I
-Landroid/nfc/cardemulation/HostApduService;->MSG_RESPONSE_APDU:I
-Landroid/nfc/cardemulation/HostApduService;->MSG_UNHANDLED:I
-Landroid/nfc/cardemulation/HostApduService;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/HostNfcFService;->KEY_DATA:Ljava/lang/String;
-Landroid/nfc/cardemulation/HostNfcFService;->KEY_MESSENGER:Ljava/lang/String;
-Landroid/nfc/cardemulation/HostNfcFService;->mMessenger:Landroid/os/Messenger;
-Landroid/nfc/cardemulation/HostNfcFService;->mNfcService:Landroid/os/Messenger;
-Landroid/nfc/cardemulation/HostNfcFService;->MSG_COMMAND_PACKET:I
-Landroid/nfc/cardemulation/HostNfcFService;->MSG_DEACTIVATED:I
-Landroid/nfc/cardemulation/HostNfcFService;->MSG_RESPONSE_PACKET:I
-Landroid/nfc/cardemulation/HostNfcFService;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFCardEmulation;-><init>(Landroid/content/Context;Landroid/nfc/INfcFCardEmulation;)V
-Landroid/nfc/cardemulation/NfcFCardEmulation;->getMaxNumOfRegisterableSystemCodes()I
-Landroid/nfc/cardemulation/NfcFCardEmulation;->getNfcFServices()Ljava/util/List;
-Landroid/nfc/cardemulation/NfcFCardEmulation;->isValidNfcid2(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/NfcFCardEmulation;->isValidSystemCode(Ljava/lang/String;)Z
-Landroid/nfc/cardemulation/NfcFCardEmulation;->mContext:Landroid/content/Context;
-Landroid/nfc/cardemulation/NfcFCardEmulation;->recoverService()V
-Landroid/nfc/cardemulation/NfcFCardEmulation;->sCardEmus:Ljava/util/HashMap;
-Landroid/nfc/cardemulation/NfcFCardEmulation;->sIsInitialized:Z
-Landroid/nfc/cardemulation/NfcFCardEmulation;->sService:Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/cardemulation/NfcFCardEmulation;->TAG:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;-><init>(Landroid/content/pm/PackageManager;Landroid/content/pm/ResolveInfo;)V
-Landroid/nfc/cardemulation/NfcFServiceInfo;-><init>(Landroid/content/pm/ResolveInfo;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)V
-Landroid/nfc/cardemulation/NfcFServiceInfo;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->DEFAULT_T3T_PMM:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->dump(Ljava/io/FileDescriptor;Ljava/io/PrintWriter;[Ljava/lang/String;)V
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getComponent()Landroid/content/ComponentName;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getDescription()Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getNfcid2()Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getSystemCode()Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getT3tPmm()Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->getUid()I
-Landroid/nfc/cardemulation/NfcFServiceInfo;->loadIcon(Landroid/content/pm/PackageManager;)Landroid/graphics/drawable/Drawable;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->loadLabel(Landroid/content/pm/PackageManager;)Ljava/lang/CharSequence;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mDescription:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mDynamicNfcid2:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mDynamicSystemCode:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mNfcid2:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mService:Landroid/content/pm/ResolveInfo;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mSystemCode:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mT3tPmm:Ljava/lang/String;
-Landroid/nfc/cardemulation/NfcFServiceInfo;->mUid:I
-Landroid/nfc/cardemulation/NfcFServiceInfo;->setOrReplaceDynamicNfcid2(Ljava/lang/String;)V
-Landroid/nfc/cardemulation/NfcFServiceInfo;->setOrReplaceDynamicSystemCode(Ljava/lang/String;)V
-Landroid/nfc/cardemulation/NfcFServiceInfo;->TAG:Ljava/lang/String;
-Landroid/nfc/ErrorCodes;-><init>()V
-Landroid/nfc/ErrorCodes;->asString(I)Ljava/lang/String;
-Landroid/nfc/ErrorCodes;->ERROR_BUFFER_TO_SMALL:I
-Landroid/nfc/ErrorCodes;->ERROR_BUSY:I
-Landroid/nfc/ErrorCodes;->ERROR_CANCELLED:I
-Landroid/nfc/ErrorCodes;->ERROR_CONNECT:I
-Landroid/nfc/ErrorCodes;->ERROR_DISCONNECT:I
-Landroid/nfc/ErrorCodes;->ERROR_INSUFFICIENT_RESOURCES:I
-Landroid/nfc/ErrorCodes;->ERROR_INVALID_PARAM:I
-Landroid/nfc/ErrorCodes;->ERROR_IO:I
-Landroid/nfc/ErrorCodes;->ERROR_NFC_ON:I
-Landroid/nfc/ErrorCodes;->ERROR_NOT_INITIALIZED:I
-Landroid/nfc/ErrorCodes;->ERROR_NOT_SUPPORTED:I
-Landroid/nfc/ErrorCodes;->ERROR_NO_SE_CONNECTED:I
-Landroid/nfc/ErrorCodes;->ERROR_READ:I
-Landroid/nfc/ErrorCodes;->ERROR_SAP_USED:I
-Landroid/nfc/ErrorCodes;->ERROR_SERVICE_NAME_USED:I
-Landroid/nfc/ErrorCodes;->ERROR_SE_ALREADY_SELECTED:I
-Landroid/nfc/ErrorCodes;->ERROR_SE_CONNECTED:I
-Landroid/nfc/ErrorCodes;->ERROR_SOCKET_CREATION:I
-Landroid/nfc/ErrorCodes;->ERROR_SOCKET_NOT_CONNECTED:I
-Landroid/nfc/ErrorCodes;->ERROR_SOCKET_OPTIONS:I
-Landroid/nfc/ErrorCodes;->ERROR_TIMEOUT:I
-Landroid/nfc/ErrorCodes;->ERROR_WRITE:I
-Landroid/nfc/ErrorCodes;->SUCCESS:I
-Landroid/nfc/IAppCallback$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/IAppCallback$Stub$Proxy;->createBeamShareData(B)Landroid/nfc/BeamShareData;
-Landroid/nfc/IAppCallback$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/IAppCallback$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/IAppCallback$Stub$Proxy;->onNdefPushComplete(B)V
-Landroid/nfc/IAppCallback$Stub$Proxy;->onTagDiscovered(Landroid/nfc/Tag;)V
-Landroid/nfc/IAppCallback$Stub;-><init>()V
-Landroid/nfc/IAppCallback$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/IAppCallback;
-Landroid/nfc/IAppCallback$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/IAppCallback$Stub;->TRANSACTION_createBeamShareData:I
-Landroid/nfc/IAppCallback$Stub;->TRANSACTION_onNdefPushComplete:I
-Landroid/nfc/IAppCallback$Stub;->TRANSACTION_onTagDiscovered:I
-Landroid/nfc/IAppCallback;->createBeamShareData(B)Landroid/nfc/BeamShareData;
-Landroid/nfc/IAppCallback;->onNdefPushComplete(B)V
-Landroid/nfc/IAppCallback;->onTagDiscovered(Landroid/nfc/Tag;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->addNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;[I)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->disable(Z)Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->disableNdefPush()Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->dispatch(Landroid/nfc/Tag;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->enable()Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->enableNdefPush()Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcAdapterExtrasInterface(Ljava/lang/String;)Landroid/nfc/INfcAdapterExtras;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcCardEmulationInterface()Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcDtaInterface(Ljava/lang/String;)Landroid/nfc/INfcDta;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcFCardEmulationInterface()Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcTagInterface()Landroid/nfc/INfcTag;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->getState()I
-Landroid/nfc/INfcAdapter$Stub$Proxy;->ignore(IILandroid/nfc/ITagRemovedCallback;)Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->invokeBeam()V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->invokeBeamInternal(Landroid/nfc/BeamShareData;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->isNdefPushEnabled()Z
-Landroid/nfc/INfcAdapter$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcAdapter$Stub$Proxy;->pausePolling(I)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->removeNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->resumePolling()V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->setAppCallback(Landroid/nfc/IAppCallback;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->setForegroundDispatch(Landroid/app/PendingIntent;[Landroid/content/IntentFilter;Landroid/nfc/TechListParcel;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->setP2pModes(II)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->setReaderMode(Landroid/os/IBinder;Landroid/nfc/IAppCallback;ILandroid/os/Bundle;)V
-Landroid/nfc/INfcAdapter$Stub$Proxy;->verifyNfcPermission()V
-Landroid/nfc/INfcAdapter$Stub;-><init>()V
-Landroid/nfc/INfcAdapter$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcAdapter;
-Landroid/nfc/INfcAdapter$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_addNfcUnlockHandler:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_disable:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_disableNdefPush:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_dispatch:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_enableNdefPush:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcAdapterExtrasInterface:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcCardEmulationInterface:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcDtaInterface:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcFCardEmulationInterface:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcTagInterface:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getState:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_ignore:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_invokeBeam:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_invokeBeamInternal:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_isNdefPushEnabled:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_pausePolling:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_removeNfcUnlockHandler:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_resumePolling:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setAppCallback:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setForegroundDispatch:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setP2pModes:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setReaderMode:I
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_verifyNfcPermission:I
-Landroid/nfc/INfcAdapter;->addNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;[I)V
-Landroid/nfc/INfcAdapter;->disable(Z)Z
-Landroid/nfc/INfcAdapter;->disableNdefPush()Z
-Landroid/nfc/INfcAdapter;->dispatch(Landroid/nfc/Tag;)V
-Landroid/nfc/INfcAdapter;->enable()Z
-Landroid/nfc/INfcAdapter;->enableNdefPush()Z
-Landroid/nfc/INfcAdapter;->getNfcAdapterExtrasInterface(Ljava/lang/String;)Landroid/nfc/INfcAdapterExtras;
-Landroid/nfc/INfcAdapter;->getNfcCardEmulationInterface()Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/INfcAdapter;->getNfcDtaInterface(Ljava/lang/String;)Landroid/nfc/INfcDta;
-Landroid/nfc/INfcAdapter;->getNfcFCardEmulationInterface()Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/INfcAdapter;->getNfcTagInterface()Landroid/nfc/INfcTag;
-Landroid/nfc/INfcAdapter;->getState()I
-Landroid/nfc/INfcAdapter;->ignore(IILandroid/nfc/ITagRemovedCallback;)Z
-Landroid/nfc/INfcAdapter;->invokeBeam()V
-Landroid/nfc/INfcAdapter;->invokeBeamInternal(Landroid/nfc/BeamShareData;)V
-Landroid/nfc/INfcAdapter;->isNdefPushEnabled()Z
-Landroid/nfc/INfcAdapter;->pausePolling(I)V
-Landroid/nfc/INfcAdapter;->removeNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;)V
-Landroid/nfc/INfcAdapter;->resumePolling()V
-Landroid/nfc/INfcAdapter;->setAppCallback(Landroid/nfc/IAppCallback;)V
-Landroid/nfc/INfcAdapter;->setForegroundDispatch(Landroid/app/PendingIntent;[Landroid/content/IntentFilter;Landroid/nfc/TechListParcel;)V
-Landroid/nfc/INfcAdapter;->setP2pModes(II)V
-Landroid/nfc/INfcAdapter;->setReaderMode(Landroid/os/IBinder;Landroid/nfc/IAppCallback;ILandroid/os/Bundle;)V
-Landroid/nfc/INfcAdapter;->verifyNfcPermission()V
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->authenticate(Ljava/lang/String;[B)V
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->close(Ljava/lang/String;Landroid/os/IBinder;)Landroid/os/Bundle;
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->getCardEmulationRoute(Ljava/lang/String;)I
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->getDriverName(Ljava/lang/String;)Ljava/lang/String;
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->open(Ljava/lang/String;Landroid/os/IBinder;)Landroid/os/Bundle;
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->setCardEmulationRoute(Ljava/lang/String;I)V
-Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->transceive(Ljava/lang/String;[B)Landroid/os/Bundle;
-Landroid/nfc/INfcAdapterExtras$Stub;-><init>()V
-Landroid/nfc/INfcAdapterExtras$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcAdapterExtras;
-Landroid/nfc/INfcAdapterExtras$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_authenticate:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_close:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_getCardEmulationRoute:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_getDriverName:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_open:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_setCardEmulationRoute:I
-Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_transceive:I
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->getAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Landroid/nfc/cardemulation/AidGroup;
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->getServices(ILjava/lang/String;)Ljava/util/List;
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->isDefaultServiceForAid(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->isDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->registerAidGroupForService(ILandroid/content/ComponentName;Landroid/nfc/cardemulation/AidGroup;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->removeAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->setDefaultForNextTap(ILandroid/content/ComponentName;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->setDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->setPreferredService(Landroid/content/ComponentName;)Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->supportsAidPrefixRegistration()Z
-Landroid/nfc/INfcCardEmulation$Stub$Proxy;->unsetPreferredService()Z
-Landroid/nfc/INfcCardEmulation$Stub;-><init>()V
-Landroid/nfc/INfcCardEmulation$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/INfcCardEmulation$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_getAidGroupForService:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_getServices:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_isDefaultServiceForAid:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_isDefaultServiceForCategory:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_registerAidGroupForService:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_removeAidGroupForService:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_setDefaultForNextTap:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_setDefaultServiceForCategory:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_setPreferredService:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_supportsAidPrefixRegistration:I
-Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_unsetPreferredService:I
-Landroid/nfc/INfcCardEmulation;->getAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Landroid/nfc/cardemulation/AidGroup;
-Landroid/nfc/INfcCardEmulation;->getServices(ILjava/lang/String;)Ljava/util/List;
-Landroid/nfc/INfcCardEmulation;->isDefaultServiceForAid(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation;->isDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation;->registerAidGroupForService(ILandroid/content/ComponentName;Landroid/nfc/cardemulation/AidGroup;)Z
-Landroid/nfc/INfcCardEmulation;->removeAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation;->setDefaultForNextTap(ILandroid/content/ComponentName;)Z
-Landroid/nfc/INfcCardEmulation;->setDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcCardEmulation;->setPreferredService(Landroid/content/ComponentName;)Z
-Landroid/nfc/INfcCardEmulation;->supportsAidPrefixRegistration()Z
-Landroid/nfc/INfcCardEmulation;->unsetPreferredService()Z
-Landroid/nfc/INfcDta$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcDta$Stub$Proxy;->disableClient()V
-Landroid/nfc/INfcDta$Stub$Proxy;->disableDta()V
-Landroid/nfc/INfcDta$Stub$Proxy;->disableServer()V
-Landroid/nfc/INfcDta$Stub$Proxy;->enableClient(Ljava/lang/String;III)Z
-Landroid/nfc/INfcDta$Stub$Proxy;->enableDta()V
-Landroid/nfc/INfcDta$Stub$Proxy;->enableServer(Ljava/lang/String;IIII)Z
-Landroid/nfc/INfcDta$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcDta$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcDta$Stub$Proxy;->registerMessageService(Ljava/lang/String;)Z
-Landroid/nfc/INfcDta$Stub;-><init>()V
-Landroid/nfc/INfcDta$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcDta;
-Landroid/nfc/INfcDta$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_disableClient:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_disableDta:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_disableServer:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_enableClient:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_enableDta:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_enableServer:I
-Landroid/nfc/INfcDta$Stub;->TRANSACTION_registerMessageService:I
-Landroid/nfc/INfcDta;->disableClient()V
-Landroid/nfc/INfcDta;->disableDta()V
-Landroid/nfc/INfcDta;->disableServer()V
-Landroid/nfc/INfcDta;->enableClient(Ljava/lang/String;III)Z
-Landroid/nfc/INfcDta;->enableDta()V
-Landroid/nfc/INfcDta;->enableServer(Ljava/lang/String;IIII)Z
-Landroid/nfc/INfcDta;->registerMessageService(Ljava/lang/String;)Z
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->disableNfcFForegroundService()Z
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->enableNfcFForegroundService(Landroid/content/ComponentName;)Z
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getMaxNumOfRegisterableSystemCodes()I
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getNfcFServices(I)Ljava/util/List;
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getNfcid2ForService(ILandroid/content/ComponentName;)Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getSystemCodeForService(ILandroid/content/ComponentName;)Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->registerSystemCodeForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->removeSystemCodeForService(ILandroid/content/ComponentName;)Z
-Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->setNfcid2ForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcFCardEmulation$Stub;-><init>()V
-Landroid/nfc/INfcFCardEmulation$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/INfcFCardEmulation$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_disableNfcFForegroundService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_enableNfcFForegroundService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getMaxNumOfRegisterableSystemCodes:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getNfcFServices:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getNfcid2ForService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getSystemCodeForService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_registerSystemCodeForService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_removeSystemCodeForService:I
-Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_setNfcid2ForService:I
-Landroid/nfc/INfcFCardEmulation;->disableNfcFForegroundService()Z
-Landroid/nfc/INfcFCardEmulation;->enableNfcFForegroundService(Landroid/content/ComponentName;)Z
-Landroid/nfc/INfcFCardEmulation;->getMaxNumOfRegisterableSystemCodes()I
-Landroid/nfc/INfcFCardEmulation;->getNfcFServices(I)Ljava/util/List;
-Landroid/nfc/INfcFCardEmulation;->getNfcid2ForService(ILandroid/content/ComponentName;)Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation;->getSystemCodeForService(ILandroid/content/ComponentName;)Ljava/lang/String;
-Landroid/nfc/INfcFCardEmulation;->registerSystemCodeForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcFCardEmulation;->removeSystemCodeForService(ILandroid/content/ComponentName;)Z
-Landroid/nfc/INfcFCardEmulation;->setNfcid2ForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z
-Landroid/nfc/INfcTag$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcTag$Stub$Proxy;->canMakeReadOnly(I)Z
-Landroid/nfc/INfcTag$Stub$Proxy;->connect(II)I
-Landroid/nfc/INfcTag$Stub$Proxy;->formatNdef(I[B)I
-Landroid/nfc/INfcTag$Stub$Proxy;->getExtendedLengthApdusSupported()Z
-Landroid/nfc/INfcTag$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcTag$Stub$Proxy;->getMaxTransceiveLength(I)I
-Landroid/nfc/INfcTag$Stub$Proxy;->getTechList(I)[I
-Landroid/nfc/INfcTag$Stub$Proxy;->getTimeout(I)I
-Landroid/nfc/INfcTag$Stub$Proxy;->isNdef(I)Z
-Landroid/nfc/INfcTag$Stub$Proxy;->isPresent(I)Z
-Landroid/nfc/INfcTag$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcTag$Stub$Proxy;->ndefIsWritable(I)Z
-Landroid/nfc/INfcTag$Stub$Proxy;->ndefMakeReadOnly(I)I
-Landroid/nfc/INfcTag$Stub$Proxy;->ndefRead(I)Landroid/nfc/NdefMessage;
-Landroid/nfc/INfcTag$Stub$Proxy;->ndefWrite(ILandroid/nfc/NdefMessage;)I
-Landroid/nfc/INfcTag$Stub$Proxy;->reconnect(I)I
-Landroid/nfc/INfcTag$Stub$Proxy;->rediscover(I)Landroid/nfc/Tag;
-Landroid/nfc/INfcTag$Stub$Proxy;->resetTimeouts()V
-Landroid/nfc/INfcTag$Stub$Proxy;->setTimeout(II)I
-Landroid/nfc/INfcTag$Stub$Proxy;->transceive(I[BZ)Landroid/nfc/TransceiveResult;
-Landroid/nfc/INfcTag$Stub;-><init>()V
-Landroid/nfc/INfcTag$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcTag;
-Landroid/nfc/INfcTag$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_canMakeReadOnly:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_connect:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_formatNdef:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_getExtendedLengthApdusSupported:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_getMaxTransceiveLength:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_getTechList:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_getTimeout:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_isNdef:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_isPresent:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefIsWritable:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefMakeReadOnly:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefRead:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefWrite:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_reconnect:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_rediscover:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_resetTimeouts:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_setTimeout:I
-Landroid/nfc/INfcTag$Stub;->TRANSACTION_transceive:I
-Landroid/nfc/INfcTag;->canMakeReadOnly(I)Z
-Landroid/nfc/INfcTag;->connect(II)I
-Landroid/nfc/INfcTag;->formatNdef(I[B)I
-Landroid/nfc/INfcTag;->getExtendedLengthApdusSupported()Z
-Landroid/nfc/INfcTag;->getMaxTransceiveLength(I)I
-Landroid/nfc/INfcTag;->getTechList(I)[I
-Landroid/nfc/INfcTag;->getTimeout(I)I
-Landroid/nfc/INfcTag;->isNdef(I)Z
-Landroid/nfc/INfcTag;->isPresent(I)Z
-Landroid/nfc/INfcTag;->ndefIsWritable(I)Z
-Landroid/nfc/INfcTag;->ndefMakeReadOnly(I)I
-Landroid/nfc/INfcTag;->ndefRead(I)Landroid/nfc/NdefMessage;
-Landroid/nfc/INfcTag;->ndefWrite(ILandroid/nfc/NdefMessage;)I
-Landroid/nfc/INfcTag;->reconnect(I)I
-Landroid/nfc/INfcTag;->rediscover(I)Landroid/nfc/Tag;
-Landroid/nfc/INfcTag;->resetTimeouts()V
-Landroid/nfc/INfcTag;->setTimeout(II)I
-Landroid/nfc/INfcTag;->transceive(I[BZ)Landroid/nfc/TransceiveResult;
-Landroid/nfc/INfcUnlockHandler$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/INfcUnlockHandler$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/INfcUnlockHandler$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/INfcUnlockHandler$Stub$Proxy;->onUnlockAttempted(Landroid/nfc/Tag;)Z
-Landroid/nfc/INfcUnlockHandler$Stub;-><init>()V
-Landroid/nfc/INfcUnlockHandler$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcUnlockHandler;
-Landroid/nfc/INfcUnlockHandler$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/INfcUnlockHandler$Stub;->TRANSACTION_onUnlockAttempted:I
-Landroid/nfc/INfcUnlockHandler;->onUnlockAttempted(Landroid/nfc/Tag;)Z
-Landroid/nfc/ITagRemovedCallback$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/nfc/ITagRemovedCallback$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/nfc/ITagRemovedCallback$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/nfc/ITagRemovedCallback$Stub$Proxy;->onTagRemoved()V
-Landroid/nfc/ITagRemovedCallback$Stub;-><init>()V
-Landroid/nfc/ITagRemovedCallback$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/ITagRemovedCallback;
-Landroid/nfc/ITagRemovedCallback$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/nfc/ITagRemovedCallback$Stub;->TRANSACTION_onTagRemoved:I
-Landroid/nfc/ITagRemovedCallback;->onTagRemoved()V
-Landroid/nfc/NdefMessage;->mRecords:[Landroid/nfc/NdefRecord;
-Landroid/nfc/NdefRecord;->bytesToString([B)Ljava/lang/StringBuilder;
-Landroid/nfc/NdefRecord;->EMPTY_BYTE_ARRAY:[B
-Landroid/nfc/NdefRecord;->ensureSanePayloadSize(J)V
-Landroid/nfc/NdefRecord;->FLAG_CF:B
-Landroid/nfc/NdefRecord;->FLAG_IL:B
-Landroid/nfc/NdefRecord;->FLAG_MB:B
-Landroid/nfc/NdefRecord;->FLAG_ME:B
-Landroid/nfc/NdefRecord;->FLAG_SR:B
-Landroid/nfc/NdefRecord;->getByteLength()I
-Landroid/nfc/NdefRecord;->MAX_PAYLOAD_SIZE:I
-Landroid/nfc/NdefRecord;->mPayload:[B
-Landroid/nfc/NdefRecord;->mTnf:S
-Landroid/nfc/NdefRecord;->mType:[B
-Landroid/nfc/NdefRecord;->parse(Ljava/nio/ByteBuffer;Z)[Landroid/nfc/NdefRecord;
-Landroid/nfc/NdefRecord;->parseWktUri()Landroid/net/Uri;
-Landroid/nfc/NdefRecord;->RTD_ANDROID_APP:[B
-Landroid/nfc/NdefRecord;->TNF_RESERVED:S
-Landroid/nfc/NdefRecord;->toUri(Z)Landroid/net/Uri;
-Landroid/nfc/NdefRecord;->URI_PREFIX_MAP:[Ljava/lang/String;
-Landroid/nfc/NdefRecord;->validateTnf(S[B[B[B)Ljava/lang/String;
-Landroid/nfc/NdefRecord;->writeToByteBuffer(Ljava/nio/ByteBuffer;ZZ)V
-Landroid/nfc/NfcActivityManager$NfcActivityState;->activity:Landroid/app/Activity;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->destroy()V
-Landroid/nfc/NfcActivityManager$NfcActivityState;->flags:I
-Landroid/nfc/NfcActivityManager$NfcActivityState;->ndefMessage:Landroid/nfc/NdefMessage;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->ndefMessageCallback:Landroid/nfc/NfcAdapter$CreateNdefMessageCallback;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->onNdefPushCompleteCallback:Landroid/nfc/NfcAdapter$OnNdefPushCompleteCallback;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->readerCallback:Landroid/nfc/NfcAdapter$ReaderCallback;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->readerModeExtras:Landroid/os/Bundle;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->readerModeFlags:I
-Landroid/nfc/NfcActivityManager$NfcActivityState;->resumed:Z
-Landroid/nfc/NfcActivityManager$NfcActivityState;->token:Landroid/os/Binder;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->uriCallback:Landroid/nfc/NfcAdapter$CreateBeamUrisCallback;
-Landroid/nfc/NfcActivityManager$NfcActivityState;->uris:[Landroid/net/Uri;
-Landroid/nfc/NfcActivityManager$NfcApplicationState;->app:Landroid/app/Application;
-Landroid/nfc/NfcActivityManager$NfcApplicationState;->refCount:I
-Landroid/nfc/NfcActivityManager$NfcApplicationState;->register()V
-Landroid/nfc/NfcActivityManager$NfcApplicationState;->unregister()V
-Landroid/nfc/NfcActivityManager;-><init>(Landroid/nfc/NfcAdapter;)V
-Landroid/nfc/NfcActivityManager;->createBeamShareData(B)Landroid/nfc/BeamShareData;
-Landroid/nfc/NfcActivityManager;->DBG:Ljava/lang/Boolean;
-Landroid/nfc/NfcActivityManager;->destroyActivityState(Landroid/app/Activity;)V
-Landroid/nfc/NfcActivityManager;->disableReaderMode(Landroid/app/Activity;)V
-Landroid/nfc/NfcActivityManager;->enableReaderMode(Landroid/app/Activity;Landroid/nfc/NfcAdapter$ReaderCallback;ILandroid/os/Bundle;)V
-Landroid/nfc/NfcActivityManager;->findActivityState(Landroid/app/Activity;)Landroid/nfc/NfcActivityManager$NfcActivityState;
-Landroid/nfc/NfcActivityManager;->findAppState(Landroid/app/Application;)Landroid/nfc/NfcActivityManager$NfcApplicationState;
-Landroid/nfc/NfcActivityManager;->findResumedActivityState()Landroid/nfc/NfcActivityManager$NfcActivityState;
-Landroid/nfc/NfcActivityManager;->getActivityState(Landroid/app/Activity;)Landroid/nfc/NfcActivityManager$NfcActivityState;
-Landroid/nfc/NfcActivityManager;->mActivities:Ljava/util/List;
-Landroid/nfc/NfcActivityManager;->mApps:Ljava/util/List;
-Landroid/nfc/NfcActivityManager;->onNdefPushComplete(B)V
-Landroid/nfc/NfcActivityManager;->onTagDiscovered(Landroid/nfc/Tag;)V
-Landroid/nfc/NfcActivityManager;->registerApplication(Landroid/app/Application;)V
-Landroid/nfc/NfcActivityManager;->requestNfcServiceCallback()V
-Landroid/nfc/NfcActivityManager;->setNdefPushContentUri(Landroid/app/Activity;[Landroid/net/Uri;)V
-Landroid/nfc/NfcActivityManager;->setNdefPushContentUriCallback(Landroid/app/Activity;Landroid/nfc/NfcAdapter$CreateBeamUrisCallback;)V
-Landroid/nfc/NfcActivityManager;->setNdefPushMessage(Landroid/app/Activity;Landroid/nfc/NdefMessage;I)V
-Landroid/nfc/NfcActivityManager;->setNdefPushMessageCallback(Landroid/app/Activity;Landroid/nfc/NfcAdapter$CreateNdefMessageCallback;I)V
-Landroid/nfc/NfcActivityManager;->setOnNdefPushCompleteCallback(Landroid/app/Activity;Landroid/nfc/NfcAdapter$OnNdefPushCompleteCallback;)V
-Landroid/nfc/NfcActivityManager;->setReaderMode(Landroid/os/Binder;ILandroid/os/Bundle;)V
-Landroid/nfc/NfcActivityManager;->TAG:Ljava/lang/String;
-Landroid/nfc/NfcActivityManager;->unregisterApplication(Landroid/app/Application;)V
-Landroid/nfc/NfcActivityManager;->verifyNfcPermission()V
-Landroid/nfc/NfcAdapter;-><init>(Landroid/content/Context;)V
-Landroid/nfc/NfcAdapter;->ACTION_HANDOVER_TRANSFER_DONE:Ljava/lang/String;
-Landroid/nfc/NfcAdapter;->ACTION_HANDOVER_TRANSFER_STARTED:Ljava/lang/String;
-Landroid/nfc/NfcAdapter;->ACTION_TAG_LEFT_FIELD:Ljava/lang/String;
-Landroid/nfc/NfcAdapter;->disableForegroundDispatchInternal(Landroid/app/Activity;Z)V
-Landroid/nfc/NfcAdapter;->dispatch(Landroid/nfc/Tag;)V
-Landroid/nfc/NfcAdapter;->enforceResumed(Landroid/app/Activity;)V
-Landroid/nfc/NfcAdapter;->EXTRA_HANDOVER_TRANSFER_STATUS:Ljava/lang/String;
-Landroid/nfc/NfcAdapter;->EXTRA_HANDOVER_TRANSFER_URI:Ljava/lang/String;
-Landroid/nfc/NfcAdapter;->getCardEmulationService()Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/NfcAdapter;->getNfcDtaInterface()Landroid/nfc/INfcDta;
-Landroid/nfc/NfcAdapter;->getNfcFCardEmulationService()Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/NfcAdapter;->getSdkVersion()I
-Landroid/nfc/NfcAdapter;->getServiceInterface()Landroid/nfc/INfcAdapter;
-Landroid/nfc/NfcAdapter;->getTagService()Landroid/nfc/INfcTag;
-Landroid/nfc/NfcAdapter;->HANDOVER_TRANSFER_STATUS_FAILURE:I
-Landroid/nfc/NfcAdapter;->HANDOVER_TRANSFER_STATUS_SUCCESS:I
-Landroid/nfc/NfcAdapter;->hasNfcFeature()Z
-Landroid/nfc/NfcAdapter;->hasNfcHceFeature()Z
-Landroid/nfc/NfcAdapter;->invokeBeam(Landroid/nfc/BeamShareData;)Z
-Landroid/nfc/NfcAdapter;->mContext:Landroid/content/Context;
-Landroid/nfc/NfcAdapter;->mForegroundDispatchListener:Landroid/app/OnActivityPausedListener;
-Landroid/nfc/NfcAdapter;->mLock:Ljava/lang/Object;
-Landroid/nfc/NfcAdapter;->mNfcActivityManager:Landroid/nfc/NfcActivityManager;
-Landroid/nfc/NfcAdapter;->mNfcUnlockHandlers:Ljava/util/HashMap;
-Landroid/nfc/NfcAdapter;->mTagRemovedListener:Landroid/nfc/ITagRemovedCallback;
-Landroid/nfc/NfcAdapter;->pausePolling(I)V
-Landroid/nfc/NfcAdapter;->resumePolling()V
-Landroid/nfc/NfcAdapter;->sCardEmulationService:Landroid/nfc/INfcCardEmulation;
-Landroid/nfc/NfcAdapter;->setP2pModes(II)V
-Landroid/nfc/NfcAdapter;->sHasNfcFeature:Z
-Landroid/nfc/NfcAdapter;->sIsInitialized:Z
-Landroid/nfc/NfcAdapter;->sNfcAdapters:Ljava/util/HashMap;
-Landroid/nfc/NfcAdapter;->sNfcFCardEmulationService:Landroid/nfc/INfcFCardEmulation;
-Landroid/nfc/NfcAdapter;->sNullContextNfcAdapter:Landroid/nfc/NfcAdapter;
-Landroid/nfc/NfcAdapter;->sTagService:Landroid/nfc/INfcTag;
-Landroid/nfc/NfcAdapter;->TAG:Ljava/lang/String;
-Landroid/nfc/NfcEvent;-><init>(Landroid/nfc/NfcAdapter;B)V
-Landroid/nfc/NfcManager;->mAdapter:Landroid/nfc/NfcAdapter;
-Landroid/nfc/Tag;-><init>([B[I[Landroid/os/Bundle;ILandroid/nfc/INfcTag;)V
-Landroid/nfc/Tag;->createMockTag([B[I[Landroid/os/Bundle;)Landroid/nfc/Tag;
-Landroid/nfc/Tag;->generateTechStringList([I)[Ljava/lang/String;
-Landroid/nfc/Tag;->getConnectedTechnology()I
-Landroid/nfc/Tag;->getTechCodeList()[I
-Landroid/nfc/Tag;->getTechCodesFromStrings([Ljava/lang/String;)[I
-Landroid/nfc/Tag;->getTechExtras(I)Landroid/os/Bundle;
-Landroid/nfc/Tag;->getTechStringToCodeMap()Ljava/util/HashMap;
-Landroid/nfc/Tag;->hasTech(I)Z
-Landroid/nfc/Tag;->mConnectedTechnology:I
-Landroid/nfc/Tag;->mServiceHandle:I
-Landroid/nfc/Tag;->mTagService:Landroid/nfc/INfcTag;
-Landroid/nfc/Tag;->mTechExtras:[Landroid/os/Bundle;
-Landroid/nfc/Tag;->mTechList:[I
-Landroid/nfc/Tag;->mTechStringList:[Ljava/lang/String;
-Landroid/nfc/Tag;->readBytesWithNull(Landroid/os/Parcel;)[B
-Landroid/nfc/Tag;->rediscover()Landroid/nfc/Tag;
-Landroid/nfc/Tag;->setConnectedTechnology(I)V
-Landroid/nfc/Tag;->setTechnologyDisconnected()V
-Landroid/nfc/Tag;->writeBytesWithNull(Landroid/os/Parcel;[B)V
-Landroid/nfc/tech/BasicTagTechnology;-><init>(Landroid/nfc/Tag;I)V
-Landroid/nfc/tech/BasicTagTechnology;->checkConnected()V
-Landroid/nfc/tech/BasicTagTechnology;->getMaxTransceiveLengthInternal()I
-Landroid/nfc/tech/BasicTagTechnology;->mIsConnected:Z
-Landroid/nfc/tech/BasicTagTechnology;->mSelectedTechnology:I
-Landroid/nfc/tech/BasicTagTechnology;->mTag:Landroid/nfc/Tag;
-Landroid/nfc/tech/BasicTagTechnology;->reconnect()V
-Landroid/nfc/tech/BasicTagTechnology;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/BasicTagTechnology;->transceive([BZ)[B
-Landroid/nfc/tech/IsoDep;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/IsoDep;->EXTRA_HIST_BYTES:Ljava/lang/String;
-Landroid/nfc/tech/IsoDep;->EXTRA_HI_LAYER_RESP:Ljava/lang/String;
-Landroid/nfc/tech/IsoDep;->mHiLayerResponse:[B
-Landroid/nfc/tech/IsoDep;->mHistBytes:[B
-Landroid/nfc/tech/IsoDep;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/MifareClassic;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/MifareClassic;->authenticate(I[BZ)Z
-Landroid/nfc/tech/MifareClassic;->isEmulated()Z
-Landroid/nfc/tech/MifareClassic;->MAX_BLOCK_COUNT:I
-Landroid/nfc/tech/MifareClassic;->MAX_SECTOR_COUNT:I
-Landroid/nfc/tech/MifareClassic;->mIsEmulated:Z
-Landroid/nfc/tech/MifareClassic;->mSize:I
-Landroid/nfc/tech/MifareClassic;->mType:I
-Landroid/nfc/tech/MifareClassic;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/MifareClassic;->validateBlock(I)V
-Landroid/nfc/tech/MifareClassic;->validateSector(I)V
-Landroid/nfc/tech/MifareClassic;->validateValueOperand(I)V
-Landroid/nfc/tech/MifareUltralight;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/MifareUltralight;->EXTRA_IS_UL_C:Ljava/lang/String;
-Landroid/nfc/tech/MifareUltralight;->MAX_PAGE_COUNT:I
-Landroid/nfc/tech/MifareUltralight;->mType:I
-Landroid/nfc/tech/MifareUltralight;->NXP_MANUFACTURER_ID:I
-Landroid/nfc/tech/MifareUltralight;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/MifareUltralight;->validatePageIndex(I)V
-Landroid/nfc/tech/Ndef;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/Ndef;->EXTRA_NDEF_CARDSTATE:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->EXTRA_NDEF_MAXLENGTH:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->EXTRA_NDEF_MSG:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->EXTRA_NDEF_TYPE:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->ICODE_SLI:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->mCardState:I
-Landroid/nfc/tech/Ndef;->mMaxNdefSize:I
-Landroid/nfc/tech/Ndef;->mNdefMsg:Landroid/nfc/NdefMessage;
-Landroid/nfc/tech/Ndef;->mNdefType:I
-Landroid/nfc/tech/Ndef;->NDEF_MODE_READ_ONLY:I
-Landroid/nfc/tech/Ndef;->NDEF_MODE_READ_WRITE:I
-Landroid/nfc/tech/Ndef;->NDEF_MODE_UNKNOWN:I
-Landroid/nfc/tech/Ndef;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/Ndef;->TYPE_1:I
-Landroid/nfc/tech/Ndef;->TYPE_2:I
-Landroid/nfc/tech/Ndef;->TYPE_3:I
-Landroid/nfc/tech/Ndef;->TYPE_4:I
-Landroid/nfc/tech/Ndef;->TYPE_ICODE_SLI:I
-Landroid/nfc/tech/Ndef;->TYPE_MIFARE_CLASSIC:I
-Landroid/nfc/tech/Ndef;->TYPE_OTHER:I
-Landroid/nfc/tech/Ndef;->UNKNOWN:Ljava/lang/String;
-Landroid/nfc/tech/NdefFormatable;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NdefFormatable;->format(Landroid/nfc/NdefMessage;Z)V
-Landroid/nfc/tech/NdefFormatable;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/NfcA;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NfcA;->EXTRA_ATQA:Ljava/lang/String;
-Landroid/nfc/tech/NfcA;->EXTRA_SAK:Ljava/lang/String;
-Landroid/nfc/tech/NfcA;->mAtqa:[B
-Landroid/nfc/tech/NfcA;->mSak:S
-Landroid/nfc/tech/NfcA;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/NfcB;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NfcB;->EXTRA_APPDATA:Ljava/lang/String;
-Landroid/nfc/tech/NfcB;->EXTRA_PROTINFO:Ljava/lang/String;
-Landroid/nfc/tech/NfcB;->mAppData:[B
-Landroid/nfc/tech/NfcB;->mProtInfo:[B
-Landroid/nfc/tech/NfcBarcode;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NfcBarcode;->EXTRA_BARCODE_TYPE:Ljava/lang/String;
-Landroid/nfc/tech/NfcBarcode;->mType:I
-Landroid/nfc/tech/NfcF;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NfcF;->EXTRA_PMM:Ljava/lang/String;
-Landroid/nfc/tech/NfcF;->EXTRA_SC:Ljava/lang/String;
-Landroid/nfc/tech/NfcF;->mManufacturer:[B
-Landroid/nfc/tech/NfcF;->mSystemCode:[B
-Landroid/nfc/tech/NfcF;->TAG:Ljava/lang/String;
-Landroid/nfc/tech/NfcV;-><init>(Landroid/nfc/Tag;)V
-Landroid/nfc/tech/NfcV;->EXTRA_DSFID:Ljava/lang/String;
-Landroid/nfc/tech/NfcV;->EXTRA_RESP_FLAGS:Ljava/lang/String;
-Landroid/nfc/tech/NfcV;->mDsfId:B
-Landroid/nfc/tech/NfcV;->mRespFlags:B
-Landroid/nfc/tech/TagTechnology;->ISO_DEP:I
-Landroid/nfc/tech/TagTechnology;->MIFARE_CLASSIC:I
-Landroid/nfc/tech/TagTechnology;->MIFARE_ULTRALIGHT:I
-Landroid/nfc/tech/TagTechnology;->NDEF:I
-Landroid/nfc/tech/TagTechnology;->NDEF_FORMATABLE:I
-Landroid/nfc/tech/TagTechnology;->NFC_A:I
-Landroid/nfc/tech/TagTechnology;->NFC_B:I
-Landroid/nfc/tech/TagTechnology;->NFC_BARCODE:I
-Landroid/nfc/tech/TagTechnology;->NFC_F:I
-Landroid/nfc/tech/TagTechnology;->NFC_V:I
-Landroid/nfc/tech/TagTechnology;->reconnect()V
-Landroid/nfc/TechListParcel;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/nfc/TechListParcel;->getTechLists()[[Ljava/lang/String;
-Landroid/nfc/TechListParcel;->mTechLists:[[Ljava/lang/String;
-Landroid/nfc/TransceiveResult;-><init>(I[B)V
-Landroid/nfc/TransceiveResult;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/nfc/TransceiveResult;->getResponseOrThrow()[B
-Landroid/nfc/TransceiveResult;->mResponseData:[B
-Landroid/nfc/TransceiveResult;->mResult:I
-Landroid/nfc/TransceiveResult;->RESULT_EXCEEDED_LENGTH:I
-Landroid/nfc/TransceiveResult;->RESULT_FAILURE:I
-Landroid/nfc/TransceiveResult;->RESULT_SUCCESS:I
-Landroid/nfc/TransceiveResult;->RESULT_TAGLOST:I
Landroid/opengl/EGL14;->eglCreatePbufferFromClientBuffer(Landroid/opengl/EGLDisplay;IJLandroid/opengl/EGLConfig;[II)Landroid/opengl/EGLSurface;
Landroid/opengl/EGL14;->_eglCreateWindowSurface(Landroid/opengl/EGLDisplay;Landroid/opengl/EGLConfig;Ljava/lang/Object;[II)Landroid/opengl/EGLSurface;
Landroid/opengl/EGL14;->_eglCreateWindowSurfaceTexture(Landroid/opengl/EGLDisplay;Landroid/opengl/EGLConfig;Ljava/lang/Object;[II)Landroid/opengl/EGLSurface;
diff --git a/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt b/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt
index f5184e7..4df1dca 100644
--- a/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt
+++ b/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt
@@ -19,7 +19,6 @@
Landroid/media/IVolumeController$Stub;->asInterface(Landroid/os/IBinder;)Landroid/media/IVolumeController;
Landroid/net/INetworkPolicyListener$Stub;-><init>()V
Landroid/net/sip/ISipSession$Stub;->asInterface(Landroid/os/IBinder;)Landroid/net/sip/ISipSession;
-Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_enable:I
Landroid/os/IPowerManager$Stub;->TRANSACTION_acquireWakeLock:I
Landroid/os/IPowerManager$Stub;->TRANSACTION_goToSleep:I
Landroid/service/euicc/IEuiccService$Stub;-><init>()V
diff --git a/core/api/current.txt b/core/api/current.txt
index 46c8f82..ce7c0440 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1602,7 +1602,6 @@
field public static final int switchTextOff = 16843628; // 0x101036c
field public static final int switchTextOn = 16843627; // 0x101036b
field public static final int syncable = 16842777; // 0x1010019
- field @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") public static final int systemUserOnly;
field public static final int tabStripEnabled = 16843453; // 0x10102bd
field public static final int tabStripLeft = 16843451; // 0x10102bb
field public static final int tabStripRight = 16843452; // 0x10102bc
@@ -4625,9 +4624,9 @@
public class ActivityManager {
method public int addAppTask(@NonNull android.app.Activity, @NonNull android.content.Intent, @Nullable android.app.ActivityManager.TaskDescription, @NonNull android.graphics.Bitmap);
+ method @FlaggedApi("android.app.app_start_info") public void addApplicationStartInfoCompletionListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
method @FlaggedApi("android.app.app_start_info") public void addStartInfoTimestamp(@IntRange(from=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START, to=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER) int, long);
method public void appNotResponding(@NonNull String);
- method @FlaggedApi("android.app.app_start_info") public void clearApplicationStartInfoCompletionListener();
method public boolean clearApplicationUserData();
method public void clearWatchHeapLimit();
method @RequiresPermission(android.Manifest.permission.DUMP) public void dumpPackageState(java.io.FileDescriptor, String);
@@ -4661,8 +4660,8 @@
method @RequiresPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES) public void killBackgroundProcesses(String);
method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int);
method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int, android.os.Bundle);
+ method @FlaggedApi("android.app.app_start_info") public void removeApplicationStartInfoCompletionListener(@NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
method @Deprecated public void restartPackage(String);
- method @FlaggedApi("android.app.app_start_info") public void setApplicationStartInfoCompletionListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
method public void setProcessStateSummary(@Nullable byte[]);
method public static void setVrThread(int);
method public void setWatchHeapLimit(long);
@@ -5318,7 +5317,6 @@
ctor @Deprecated public AutomaticZenRule(String, android.content.ComponentName, android.net.Uri, int, boolean);
ctor public AutomaticZenRule(@NonNull String, @Nullable android.content.ComponentName, @Nullable android.content.ComponentName, @NonNull android.net.Uri, @Nullable android.service.notification.ZenPolicy, int, boolean);
ctor public AutomaticZenRule(android.os.Parcel);
- method @FlaggedApi("android.app.modes_api") public boolean canUpdate();
method public int describeContents();
method public android.net.Uri getConditionId();
method @Nullable public android.content.ComponentName getConfigurationActivity();
@@ -10387,6 +10385,7 @@
method @CheckResult(suggest="#enforceCallingPermission(String,String)") public abstract int checkCallingPermission(@NonNull String);
method @CheckResult(suggest="#enforceCallingUriPermission(Uri,int,String)") public abstract int checkCallingUriPermission(android.net.Uri, int);
method @NonNull public int[] checkCallingUriPermissions(@NonNull java.util.List<android.net.Uri>, int);
+ method @FlaggedApi("android.security.content_uri_permission_apis") public int checkContentUriPermissionFull(@NonNull android.net.Uri, int, int, int);
method @CheckResult(suggest="#enforcePermission(String,int,int,String)") public abstract int checkPermission(@NonNull String, int, int);
method public abstract int checkSelfPermission(@NonNull String);
method @CheckResult(suggest="#enforceUriPermission(Uri,int,int,String)") public abstract int checkUriPermission(android.net.Uri, int, int, int);
@@ -10545,6 +10544,7 @@
field public static final int BIND_INCLUDE_CAPABILITIES = 4096; // 0x1000
field public static final int BIND_NOT_FOREGROUND = 4; // 0x4
field public static final int BIND_NOT_PERCEPTIBLE = 256; // 0x100
+ field @FlaggedApi("android.content.flags.enable_bind_package_isolated_process") public static final int BIND_PACKAGE_ISOLATED_PROCESS = 16384; // 0x4000
field public static final int BIND_SHARED_ISOLATED_PROCESS = 8192; // 0x2000
field public static final int BIND_WAIVE_PRIORITY = 32; // 0x20
field public static final String BIOMETRIC_SERVICE = "biometric";
@@ -18752,21 +18752,21 @@
method @Nullable public java.security.Signature getSignature();
}
- @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentListItem {
+ @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentItem {
}
- @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem {
- ctor public PromptContentListItemBulletedText(@NonNull CharSequence);
+ @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem {
+ ctor public PromptContentItemBulletedText(@NonNull CharSequence);
method public int describeContents();
method public void writeToParcel(@NonNull android.os.Parcel, int);
- field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentListItemBulletedText> CREATOR;
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemBulletedText> CREATOR;
}
- @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem {
- ctor public PromptContentListItemPlainText(@NonNull CharSequence);
+ @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem {
+ ctor public PromptContentItemPlainText(@NonNull CharSequence);
method public int describeContents();
method public void writeToParcel(@NonNull android.os.Parcel, int);
- field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentListItemPlainText> CREATOR;
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemPlainText> CREATOR;
}
@FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentView {
@@ -18775,7 +18775,7 @@
@FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView {
method public int describeContents();
method @Nullable public CharSequence getDescription();
- method @NonNull public java.util.List<android.hardware.biometrics.PromptContentListItem> getListItems();
+ method @NonNull public java.util.List<android.hardware.biometrics.PromptContentItem> getListItems();
method public static int getMaxEachItemCharacterNumber();
method public static int getMaxItemCount();
method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -18784,7 +18784,8 @@
public static final class PromptVerticalListContentView.Builder {
ctor public PromptVerticalListContentView.Builder();
- method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentListItem);
+ method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentItem);
+ method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentItem, int);
method @NonNull public android.hardware.biometrics.PromptVerticalListContentView build();
method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull CharSequence);
}
@@ -23340,6 +23341,7 @@
field public static final String KEY_HDR10_PLUS_INFO = "hdr10-plus-info";
field public static final String KEY_HDR_STATIC_INFO = "hdr-static-info";
field public static final String KEY_HEIGHT = "height";
+ field @FlaggedApi("com.android.media.codec.flags.codec_importance") public static final String KEY_IMPORTANCE = "importance";
field public static final String KEY_INTRA_REFRESH_PERIOD = "intra-refresh-period";
field public static final String KEY_IS_ADTS = "is-adts";
field public static final String KEY_IS_AUTOSELECT = "is-autoselect";
@@ -26668,12 +26670,16 @@
public static final class TvContract.Channels implements android.media.tv.TvContract.BaseTvColumns {
method @Nullable public static String getVideoResolution(String);
+ field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_INVISIBLE = 2; // 0x2
+ field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_NUMERIC_SELECTABLE_ONLY = 1; // 0x1
+ field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_VISIBLE = 0; // 0x0
field public static final String COLUMN_APP_LINK_COLOR = "app_link_color";
field public static final String COLUMN_APP_LINK_ICON_URI = "app_link_icon_uri";
field public static final String COLUMN_APP_LINK_INTENT_URI = "app_link_intent_uri";
field public static final String COLUMN_APP_LINK_POSTER_ART_URI = "app_link_poster_art_uri";
field public static final String COLUMN_APP_LINK_TEXT = "app_link_text";
field public static final String COLUMN_BROADCAST_GENRE = "broadcast_genre";
+ field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final String COLUMN_BROADCAST_VISIBILITY_TYPE = "broadcast_visibility_type";
field public static final String COLUMN_BROWSABLE = "browsable";
field public static final String COLUMN_CHANNEL_LIST_ID = "channel_list_id";
field public static final String COLUMN_DESCRIPTION = "description";
@@ -40511,7 +40517,6 @@
public final class ZenPolicy implements android.os.Parcelable {
method public int describeContents();
- method @FlaggedApi("android.app.modes_api") public int getAllowedChannels();
method public int getPriorityCallSenders();
method public int getPriorityCategoryAlarms();
method public int getPriorityCategoryCalls();
@@ -40522,6 +40527,7 @@
method public int getPriorityCategoryReminders();
method public int getPriorityCategoryRepeatCallers();
method public int getPriorityCategorySystem();
+ method @FlaggedApi("android.app.modes_api") public int getPriorityChannels();
method public int getPriorityConversationSenders();
method public int getPriorityMessageSenders();
method public int getVisualEffectAmbient();
@@ -40532,9 +40538,6 @@
method public int getVisualEffectPeek();
method public int getVisualEffectStatusBar();
method public void writeToParcel(android.os.Parcel, int);
- field @FlaggedApi("android.app.modes_api") public static final int CHANNEL_TYPE_NONE = 2; // 0x2
- field @FlaggedApi("android.app.modes_api") public static final int CHANNEL_TYPE_PRIORITY = 1; // 0x1
- field @FlaggedApi("android.app.modes_api") public static final int CHANNEL_TYPE_UNSET = 0; // 0x0
field public static final int CONVERSATION_SENDERS_ANYONE = 1; // 0x1
field public static final int CONVERSATION_SENDERS_IMPORTANT = 2; // 0x2
field public static final int CONVERSATION_SENDERS_NONE = 3; // 0x3
@@ -40555,11 +40558,11 @@
method @NonNull public android.service.notification.ZenPolicy.Builder allowAlarms(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowAllSounds();
method @NonNull public android.service.notification.ZenPolicy.Builder allowCalls(int);
- method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder allowChannels(int);
method @NonNull public android.service.notification.ZenPolicy.Builder allowConversations(int);
method @NonNull public android.service.notification.ZenPolicy.Builder allowEvents(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowMedia(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowMessages(int);
+ method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder allowPriorityChannels(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowReminders(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowRepeatCallers(boolean);
method @NonNull public android.service.notification.ZenPolicy.Builder allowSystem(boolean);
@@ -41655,6 +41658,7 @@
method public void sendEvent(@NonNull String, @NonNull android.os.Bundle);
method public void setActive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method public void setInactive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ method @FlaggedApi("com.android.server.telecom.flags.set_mute_state") public void setMuteState(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method public void startCallStreaming(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index bd4ecf2..5296712 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -622,6 +622,7 @@
field public static final String OPSTR_ACCEPT_HANDOVER = "android:accept_handover";
field public static final String OPSTR_ACCESS_ACCESSIBILITY = "android:access_accessibility";
field public static final String OPSTR_ACCESS_NOTIFICATIONS = "android:access_notifications";
+ field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String OPSTR_ACCESS_RESTRICTED_SETTINGS = "android:access_restricted_settings";
field public static final String OPSTR_ACTIVATE_PLATFORM_VPN = "android:activate_platform_vpn";
field public static final String OPSTR_ACTIVATE_VPN = "android:activate_vpn";
field public static final String OPSTR_ASSIST_SCREENSHOT = "android:assist_screenshot";
@@ -3221,6 +3222,7 @@
method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
method @NonNull public android.content.Context createContext();
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback);
+ method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
method @Deprecated @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig);
@@ -3229,6 +3231,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig);
method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig);
+ method @FlaggedApi("android.companion.virtual.flags.virtual_stylus") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualStylus createVirtualStylus(@NonNull android.hardware.input.VirtualStylusConfig);
method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig);
method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
method public int getDeviceId();
@@ -3274,6 +3277,7 @@
field @Deprecated public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
field @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3
field public static final int POLICY_TYPE_AUDIO = 1; // 0x1
+ field @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final int POLICY_TYPE_CAMERA = 5; // 0x5
field @FlaggedApi("android.companion.virtual.flags.cross_device_clipboard") public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4
field public static final int POLICY_TYPE_RECENTS = 2; // 0x2
field public static final int POLICY_TYPE_SENSORS = 0; // 0x0
@@ -3356,30 +3360,38 @@
@FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback {
method public default void onProcessCaptureRequest(int, long);
method public void onStreamClosed(int);
- method public void onStreamConfigured(int, @NonNull android.view.Surface, @NonNull android.companion.virtual.camera.VirtualCameraStreamConfig);
+ method public void onStreamConfigured(int, @NonNull android.view.Surface, @IntRange(from=1) int, @IntRange(from=1) int, int);
}
@FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraConfig implements android.os.Parcelable {
method public int describeContents();
+ method public int getLensFacing();
method @NonNull public String getName();
+ method public int getSensorOrientation();
method @NonNull public java.util.Set<android.companion.virtual.camera.VirtualCameraStreamConfig> getStreamConfigs();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraConfig> CREATOR;
+ field public static final int SENSOR_ORIENTATION_0 = 0; // 0x0
+ field public static final int SENSOR_ORIENTATION_180 = 180; // 0xb4
+ field public static final int SENSOR_ORIENTATION_270 = 270; // 0x10e
+ field public static final int SENSOR_ORIENTATION_90 = 90; // 0x5a
}
@FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder {
ctor public VirtualCameraConfig.Builder();
- method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(int, int, int);
+ method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int, @IntRange(from=1) int);
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build();
+ method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setLensFacing(int);
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setName(@NonNull String);
+ method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setSensorOrientation(int);
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback);
}
@FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable {
- ctor public VirtualCameraStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int);
method public int describeContents();
method public int getFormat();
method @IntRange(from=1) public int getHeight();
+ method @IntRange(from=1) public int getMaximumFramesPerSecond();
method @IntRange(from=1) public int getWidth();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraStreamConfig> CREATOR;
@@ -3530,6 +3542,7 @@
field public static final String CLOUDSEARCH_SERVICE = "cloudsearch";
field public static final String CONTENT_SUGGESTIONS_SERVICE = "content_suggestions";
field public static final String CONTEXTHUB_SERVICE = "contexthub";
+ field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation";
field public static final String ETHERNET_SERVICE = "ethernet";
field public static final String EUICC_CARD_SERVICE = "euicc_card";
field public static final String FONT_SERVICE = "font";
@@ -4208,11 +4221,14 @@
field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.UserProperties> CREATOR;
field public static final int CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT = 1; // 0x1
field public static final int CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION = 0; // 0x0
+ field public static final int CROSS_PROFILE_CONTENT_SHARING_UNKNOWN = -1; // 0xffffffff
field public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2; // 0x2
field public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1; // 0x1
field public static final int SHOW_IN_QUIET_MODE_PAUSED = 0; // 0x0
+ field public static final int SHOW_IN_QUIET_MODE_UNKNOWN = -1; // 0xffffffff
field public static final int SHOW_IN_SHARING_SURFACES_NO = 2; // 0x2
field public static final int SHOW_IN_SHARING_SURFACES_SEPARATE = 1; // 0x1
+ field public static final int SHOW_IN_SHARING_SURFACES_UNKNOWN = -1; // 0xffffffff
field public static final int SHOW_IN_SHARING_SURFACES_WITH_PARENT = 0; // 0x0
}
@@ -5304,6 +5320,78 @@
method @NonNull public android.hardware.input.VirtualNavigationTouchpadConfig build();
}
+ @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public class VirtualStylus implements java.io.Closeable {
+ method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+ method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendButtonEvent(@NonNull android.hardware.input.VirtualStylusButtonEvent);
+ method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendMotionEvent(@NonNull android.hardware.input.VirtualStylusMotionEvent);
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusButtonEvent implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getAction();
+ method public int getButtonCode();
+ method public long getEventTimeNanos();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int ACTION_BUTTON_PRESS = 11; // 0xb
+ field public static final int ACTION_BUTTON_RELEASE = 12; // 0xc
+ field public static final int BUTTON_PRIMARY = 32; // 0x20
+ field public static final int BUTTON_SECONDARY = 64; // 0x40
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusButtonEvent> CREATOR;
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusButtonEvent.Builder {
+ ctor public VirtualStylusButtonEvent.Builder();
+ method @NonNull public android.hardware.input.VirtualStylusButtonEvent build();
+ method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setAction(int);
+ method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setButtonCode(int);
+ method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setEventTimeNanos(long);
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getHeight();
+ method public int getWidth();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusConfig> CREATOR;
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualStylusConfig.Builder> {
+ ctor public VirtualStylusConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int);
+ method @NonNull public android.hardware.input.VirtualStylusConfig build();
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusMotionEvent implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getAction();
+ method public long getEventTimeNanos();
+ method public int getPressure();
+ method public int getTiltX();
+ method public int getTiltY();
+ method public int getToolType();
+ method public int getX();
+ method public int getY();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int ACTION_DOWN = 0; // 0x0
+ field public static final int ACTION_MOVE = 2; // 0x2
+ field public static final int ACTION_UP = 1; // 0x1
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusMotionEvent> CREATOR;
+ field public static final int TOOL_TYPE_ERASER = 4; // 0x4
+ field public static final int TOOL_TYPE_STYLUS = 2; // 0x2
+ }
+
+ @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusMotionEvent.Builder {
+ ctor public VirtualStylusMotionEvent.Builder();
+ method @NonNull public android.hardware.input.VirtualStylusMotionEvent build();
+ method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setAction(int);
+ method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setEventTimeNanos(long);
+ method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setPressure(@IntRange(from=0x0, to=0xff) int);
+ method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setTiltX(@IntRange(from=0xffffffa6, to=0x5a) int);
+ method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setTiltY(@IntRange(from=0xffffffa6, to=0x5a) int);
+ method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setToolType(int);
+ method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setX(int);
+ method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setY(int);
+ }
+
public final class VirtualTouchEvent implements android.os.Parcelable {
method public int describeContents();
method public int getAction();
@@ -14471,6 +14559,7 @@
field public static final int EVENT_SERVICE_STATE_CHANGED = 1; // 0x1
field public static final int EVENT_SIGNAL_STRENGTHS_CHANGED = 9; // 0x9
field public static final int EVENT_SIGNAL_STRENGTH_CHANGED = 2; // 0x2
+ field @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41; // 0x29
field @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_SRVCC_STATE_CHANGED = 16; // 0x10
field public static final int EVENT_USER_MOBILE_DATA_STATE_CHANGED = 20; // 0x14
field @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_VOICE_ACTIVATION_STATE_CHANGED = 18; // 0x12
@@ -14517,6 +14606,10 @@
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onRadioPowerStateChanged(int);
}
+ @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") public static interface TelephonyCallback.SimultaneousCellularCallingSupportListener {
+ method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onSimultaneousCellularCallingSubscriptionsChanged(@NonNull java.util.Set<java.lang.Integer>);
+ }
+
public static interface TelephonyCallback.SrvccStateListener {
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onSrvccStateChanged(int);
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index bbe03a3..0d1d8d7 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -284,16 +284,6 @@
method public default void onOpActiveChanged(@NonNull String, int, @NonNull String, @Nullable String, boolean, int, int);
}
- public final class AutomaticZenRule implements android.os.Parcelable {
- method @FlaggedApi("android.app.modes_api") public int getUserModifiedFields();
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_INTERRUPTION_FILTER = 2; // 0x2
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_NAME = 1; // 0x1
- }
-
- @FlaggedApi("android.app.modes_api") public static final class AutomaticZenRule.Builder {
- method @FlaggedApi("android.app.modes_api") @NonNull public android.app.AutomaticZenRule.Builder setUserModifiedFields(int);
- }
-
public class BroadcastOptions extends android.app.ComponentOptions {
ctor public BroadcastOptions();
ctor public BroadcastOptions(@NonNull android.os.Bundle);
@@ -1177,6 +1167,7 @@
method public int getShowInLauncher();
field public static final int SHOW_IN_LAUNCHER_NO = 2; // 0x2
field public static final int SHOW_IN_LAUNCHER_SEPARATE = 1; // 0x1
+ field public static final int SHOW_IN_LAUNCHER_UNKNOWN = -1; // 0xffffffff
field public static final int SHOW_IN_LAUNCHER_WITH_PARENT = 0; // 0x0
}
@@ -3021,47 +3012,8 @@
method @Deprecated public boolean isBound();
}
- @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable {
- method public int getUserModifiedFields();
- field public static final int FIELD_DIM_WALLPAPER = 4; // 0x4
- field public static final int FIELD_DISABLE_AUTO_BRIGHTNESS = 16; // 0x10
- field public static final int FIELD_DISABLE_TAP_TO_WAKE = 32; // 0x20
- field public static final int FIELD_DISABLE_TILT_TO_WAKE = 64; // 0x40
- field public static final int FIELD_DISABLE_TOUCH = 128; // 0x80
- field public static final int FIELD_GRAYSCALE = 1; // 0x1
- field public static final int FIELD_MAXIMIZE_DOZE = 512; // 0x200
- field public static final int FIELD_MINIMIZE_RADIO_USAGE = 256; // 0x100
- field public static final int FIELD_NIGHT_MODE = 8; // 0x8
- field public static final int FIELD_SUPPRESS_AMBIENT_DISPLAY = 2; // 0x2
- }
-
- @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder {
- method @NonNull public android.service.notification.ZenDeviceEffects.Builder setUserModifiedFields(int);
- }
-
- public final class ZenPolicy implements android.os.Parcelable {
- method @FlaggedApi("android.app.modes_api") public int getUserModifiedFields();
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_ALLOW_CHANNELS = 8; // 0x8
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_CALLS = 2; // 0x2
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_CONVERSATIONS = 4; // 0x4
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_MESSAGES = 1; // 0x1
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 128; // 0x80
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 32; // 0x20
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 256; // 0x100
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 64; // 0x40
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 512; // 0x200
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_AMBIENT = 32768; // 0x8000
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_BADGE = 16384; // 0x4000
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1024; // 0x400
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_LIGHTS = 2048; // 0x800
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 65536; // 0x10000
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_PEEK = 4096; // 0x1000
- field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 8192; // 0x2000
- }
-
public static final class ZenPolicy.Builder {
ctor public ZenPolicy.Builder(@Nullable android.service.notification.ZenPolicy);
- method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder setUserModifiedFields(int);
}
}
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 6b7f4880..1fd49ef 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -3574,7 +3574,7 @@
* foreground. This may be running a window that is behind the current
* foreground (so paused and with its state saved, not interacting with
* the user, but visible to them to some degree); it may also be running
- * other services under the system's control that it inconsiders important.
+ * other services under the system's control that it considers important.
*/
public static final int IMPORTANCE_VISIBLE = 200;
@@ -3646,9 +3646,9 @@
public static final int IMPORTANCE_CANT_SAVE_STATE = 350;
/**
- * Constant for {@link #importance}: This process process contains
- * cached code that is expendable, not actively running any app components
- * we care about.
+ * Constant for {@link #importance}: This process contains cached code
+ * that is expendable, not actively running any app components we care
+ * about.
*/
public static final int IMPORTANCE_CACHED = 400;
@@ -4052,10 +4052,28 @@
}
}
+ private final ArrayList<AppStartInfoCallbackWrapper> mAppStartInfoCallbacks =
+ new ArrayList<>();
+ @Nullable
+ private IApplicationStartInfoCompleteListener mAppStartInfoCompleteListener = null;
+
+ private static final class AppStartInfoCallbackWrapper {
+ @NonNull final Executor mExecutor;
+ @NonNull final Consumer<ApplicationStartInfo> mListener;
+
+ AppStartInfoCallbackWrapper(@NonNull final Executor executor,
+ @NonNull final Consumer<ApplicationStartInfo> listener) {
+ mExecutor = executor;
+ mListener = listener;
+ }
+ }
+
/**
- * Sets a callback to be notified when the {@link ApplicationStartInfo} records of this startup
+ * Adds a callback to be notified when the {@link ApplicationStartInfo} records of this startup
* are complete.
*
+ * <p class="note"> Note: callback will be removed automatically after being triggered.</p>
+ *
* <p class="note"> Note: callback will not wait for {@link Activity#reportFullyDrawn} to occur.
* Timestamp for fully drawn may be added after callback occurs. Set callback after invoking
* {@link Activity#reportFullyDrawn} if timestamp for fully drawn is required.</p>
@@ -4073,33 +4091,77 @@
* @throws IllegalArgumentException if executor or listener are null.
*/
@FlaggedApi(Flags.FLAG_APP_START_INFO)
- public void setApplicationStartInfoCompletionListener(@NonNull final Executor executor,
+ public void addApplicationStartInfoCompletionListener(@NonNull final Executor executor,
@NonNull final Consumer<ApplicationStartInfo> listener) {
Preconditions.checkNotNull(executor, "executor cannot be null");
Preconditions.checkNotNull(listener, "listener cannot be null");
- IApplicationStartInfoCompleteListener callback =
- new IApplicationStartInfoCompleteListener.Stub() {
- @Override
- public void onApplicationStartInfoComplete(ApplicationStartInfo applicationStartInfo) {
- executor.execute(() -> listener.accept(applicationStartInfo));
+ synchronized (mAppStartInfoCallbacks) {
+ for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) {
+ if (listener.equals(mAppStartInfoCallbacks.get(i).mListener)) {
+ return;
+ }
}
- };
- try {
- getService().setApplicationStartInfoCompleteListener(callback, mContext.getUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ if (mAppStartInfoCompleteListener == null) {
+ mAppStartInfoCompleteListener = new IApplicationStartInfoCompleteListener.Stub() {
+ @Override
+ public void onApplicationStartInfoComplete(
+ ApplicationStartInfo applicationStartInfo) {
+ synchronized (mAppStartInfoCallbacks) {
+ for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) {
+ final AppStartInfoCallbackWrapper callback =
+ mAppStartInfoCallbacks.get(i);
+ callback.mExecutor.execute(() -> callback.mListener.accept(
+ applicationStartInfo));
+ }
+ mAppStartInfoCallbacks.clear();
+ mAppStartInfoCompleteListener = null;
+ }
+ }
+ };
+ boolean succeeded = false;
+ try {
+ getService().addApplicationStartInfoCompleteListener(
+ mAppStartInfoCompleteListener, mContext.getUserId());
+ succeeded = true;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ if (succeeded) {
+ mAppStartInfoCallbacks.add(new AppStartInfoCallbackWrapper(executor, listener));
+ } else {
+ mAppStartInfoCompleteListener = null;
+ mAppStartInfoCallbacks.clear();
+ }
+ } else {
+ mAppStartInfoCallbacks.add(new AppStartInfoCallbackWrapper(executor, listener));
+ }
}
}
/**
- * Removes the callback set by {@link #setApplicationStartInfoCompletionListener} if there is one.
+ * Removes the provided callback set by {@link #addApplicationStartInfoCompletionListener}.
*/
@FlaggedApi(Flags.FLAG_APP_START_INFO)
- public void clearApplicationStartInfoCompletionListener() {
- try {
- getService().clearApplicationStartInfoCompleteListener(mContext.getUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ public void removeApplicationStartInfoCompletionListener(
+ @NonNull final Consumer<ApplicationStartInfo> listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ synchronized (mAppStartInfoCallbacks) {
+ for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) {
+ final AppStartInfoCallbackWrapper callback = mAppStartInfoCallbacks.get(i);
+ if (listener.equals(callback.mListener)) {
+ mAppStartInfoCallbacks.remove(i);
+ break;
+ }
+ }
+ if (mAppStartInfoCompleteListener != null && mAppStartInfoCallbacks.isEmpty()) {
+ try {
+ getService().removeApplicationStartInfoCompleteListener(
+ mAppStartInfoCompleteListener, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mAppStartInfoCompleteListener = null;
+ }
}
}
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 1db1caf..669baf9 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -2180,6 +2180,8 @@
*
* @hide
*/
+ @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+ @SystemApi
public static final String OPSTR_ACCESS_RESTRICTED_SETTINGS =
"android:access_restricted_settings";
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index 656feb0..cc3eac1 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -39,12 +39,17 @@
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.Iterator;
import java.util.Map;
-import java.util.Set;
/**
- * Provide information related to a processes startup.
+ * Describes information related to an application process's startup.
+ *
+ * <p>
+ * Many aspects concerning why and how an applications process was started are valuable for apps
+ * both for logging and for potential behavior changes. Reason for process start, start type,
+ * start times, throttling, and other useful diagnostic data can be obtained from
+ * {@link ApplicationStartInfo} records.
+ * </p>
*/
@FlaggedApi(Flags.FLAG_APP_START_INFO)
public final class ApplicationStartInfo implements Parcelable {
@@ -552,13 +557,12 @@
dest.writeInt(mDefiningUid);
dest.writeString(mProcessName);
dest.writeInt(mReason);
- dest.writeInt(mStartupTimestampsNs.size());
- Set<Map.Entry<Integer, Long>> timestampEntrySet = mStartupTimestampsNs.entrySet();
- Iterator<Map.Entry<Integer, Long>> iter = timestampEntrySet.iterator();
- while (iter.hasNext()) {
- Map.Entry<Integer, Long> entry = iter.next();
- dest.writeInt(entry.getKey());
- dest.writeLong(entry.getValue());
+ dest.writeInt(mStartupTimestampsNs == null ? 0 : mStartupTimestampsNs.size());
+ if (mStartupTimestampsNs != null) {
+ for (int i = 0; i < mStartupTimestampsNs.size(); i++) {
+ dest.writeInt(mStartupTimestampsNs.keyAt(i));
+ dest.writeLong(mStartupTimestampsNs.valueAt(i));
+ }
}
dest.writeInt(mStartType);
dest.writeParcelable(mStartIntent, flags);
@@ -740,13 +744,11 @@
sb.append(" intent=").append(mStartIntent.toString())
.append('\n');
}
- if (mStartupTimestampsNs.size() > 0) {
+ if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) {
sb.append(" timestamps: ");
- Set<Map.Entry<Integer, Long>> timestampEntrySet = mStartupTimestampsNs.entrySet();
- Iterator<Map.Entry<Integer, Long>> iter = timestampEntrySet.iterator();
- while (iter.hasNext()) {
- Map.Entry<Integer, Long> entry = iter.next();
- sb.append(entry.getKey()).append("=").append(entry.getValue()).append(" ");
+ for (int i = 0; i < mStartupTimestampsNs.size(); i++) {
+ sb.append(mStartupTimestampsNs.keyAt(i)).append("=").append(mStartupTimestampsNs
+ .valueAt(i)).append(" ");
}
sb.append('\n');
}
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index 5b354fc..d57a4e5 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -23,7 +23,6 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.TestApi;
import android.app.NotificationManager.InterruptionFilter;
import android.content.ComponentName;
import android.net.Uri;
@@ -113,8 +112,8 @@
@Retention(RetentionPolicy.SOURCE)
public @interface Type {}
- /** Used to track which rule variables have been modified by the user.
- * Should be checked against the bitmask {@link #getUserModifiedFields()}.
+ /**
+ * Enum for the user-modifiable fields in this object.
* @hide
*/
@IntDef(flag = true, prefix = { "FIELD_" }, value = {
@@ -128,13 +127,11 @@
* @hide
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- @TestApi
public static final int FIELD_NAME = 1 << 0;
/**
* @hide
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- @TestApi
public static final int FIELD_INTERRUPTION_FILTER = 1 << 1;
private boolean enabled;
@@ -153,7 +150,6 @@
private int mIconResId;
private String mTriggerDescription;
private boolean mAllowManualInvocation;
- private @ModifiableField int mUserModifiedFields; // Bitwise representation
/**
* The maximum string length for any string contained in this automatic zen rule. This pertains
@@ -256,7 +252,6 @@
mIconResId = source.readInt();
mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
mType = source.readInt();
- mUserModifiedFields = source.readInt();
}
}
@@ -307,8 +302,7 @@
* Returns whether this rule's name has been modified by the user.
* @hide
*/
- // TODO: b/310620812 - Replace with mUserModifiedFields & FIELD_NAME once
- // FLAG_MODES_API is inlined.
+ // TODO: b/310620812 - Consider removing completely. Seems not be used anywhere except tests.
public boolean isModified() {
return mModified;
}
@@ -506,32 +500,6 @@
return type;
}
- /**
- * Gets the bitmask representing which fields are user modified. Bits are set using
- * {@link ModifiableField}.
- * @hide
- */
- @FlaggedApi(Flags.FLAG_MODES_API)
- @TestApi
- public @ModifiableField int getUserModifiedFields() {
- return mUserModifiedFields;
- }
-
- /**
- * Returns {@code true} if the {@link AutomaticZenRule} can be updated.
- * When this returns {@code false}, calls to
- * {@link NotificationManager#updateAutomaticZenRule(String, AutomaticZenRule)}) with this rule
- * will ignore changes to user-configurable fields.
- */
- @FlaggedApi(Flags.FLAG_MODES_API)
- public boolean canUpdate() {
- // The rule is considered updateable if its bitmask has no user modifications, and
- // the bitmasks of the policy and device effects have no modification.
- return mUserModifiedFields == 0
- && (mZenPolicy == null || mZenPolicy.getUserModifiedFields() == 0)
- && (mDeviceEffects == null || mDeviceEffects.getUserModifiedFields() == 0);
- }
-
@Override
public int describeContents() {
return 0;
@@ -560,7 +528,6 @@
dest.writeInt(mIconResId);
dest.writeString(mTriggerDescription);
dest.writeInt(mType);
- dest.writeInt(mUserModifiedFields);
}
}
@@ -582,16 +549,14 @@
.append(",allowManualInvocation=").append(mAllowManualInvocation)
.append(",iconResId=").append(mIconResId)
.append(",triggerDescription=").append(mTriggerDescription)
- .append(",type=").append(mType)
- .append(",userModifiedFields=")
- .append(modifiedFieldsToString(mUserModifiedFields));
+ .append(",type=").append(mType);
}
return sb.append(']').toString();
}
- @FlaggedApi(Flags.FLAG_MODES_API)
- private String modifiedFieldsToString(int bitmask) {
+ /** @hide */
+ public static String fieldsToString(@ModifiableField int bitmask) {
ArrayList<String> modified = new ArrayList<>();
if ((bitmask & FIELD_NAME) != 0) {
modified.add("FIELD_NAME");
@@ -623,8 +588,7 @@
&& other.mAllowManualInvocation == mAllowManualInvocation
&& other.mIconResId == mIconResId
&& Objects.equals(other.mTriggerDescription, mTriggerDescription)
- && other.mType == mType
- && other.mUserModifiedFields == mUserModifiedFields;
+ && other.mType == mType;
}
return finalEquals;
}
@@ -634,8 +598,7 @@
if (Flags.modesApi()) {
return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
configurationActivity, mZenPolicy, mDeviceEffects, mModified, creationTime,
- mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType,
- mUserModifiedFields);
+ mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType);
}
return Objects.hash(enabled, name, interruptionFilter, conditionId, owner,
configurationActivity, mZenPolicy, mModified, creationTime, mPkg);
@@ -704,7 +667,6 @@
private boolean mAllowManualInvocation;
private long mCreationTime;
private String mPkg;
- private @ModifiableField int mUserModifiedFields;
public Builder(@NonNull AutomaticZenRule rule) {
mName = rule.getName();
@@ -721,7 +683,6 @@
mAllowManualInvocation = rule.isManualInvocationAllowed();
mCreationTime = rule.getCreationTime();
mPkg = rule.getPackageName();
- mUserModifiedFields = rule.mUserModifiedFields;
}
public Builder(@NonNull String name, @NonNull Uri conditionId) {
@@ -848,19 +809,6 @@
return this;
}
- /**
- * Sets the bitmask representing which fields have been user-modified.
- * This method should not be used outside of tests. The value of userModifiedFields
- * should be set based on what values are changed when a rule is populated or updated..
- * @hide
- */
- @FlaggedApi(Flags.FLAG_MODES_API)
- @TestApi
- public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) {
- mUserModifiedFields = userModifiedFields;
- return this;
- }
-
public @NonNull AutomaticZenRule build() {
AutomaticZenRule rule = new AutomaticZenRule(mName, mOwner, mConfigurationActivity,
mConditionId, mPolicy, mInterruptionFilter, mEnabled);
@@ -871,7 +819,6 @@
rule.mIconResId = mIconResId;
rule.mAllowManualInvocation = mAllowManualInvocation;
rule.setPackageName(mPkg);
- rule.mUserModifiedFields = mUserModifiedFields;
return rule;
}
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index edeec77..ed00d9c 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -2409,6 +2409,17 @@
}
}
+ @Override
+ public int checkContentUriPermissionFull(Uri uri, int pid, int uid, int modeFlags) {
+ try {
+ return ActivityManager.getService().checkContentUriPermissionFull(
+ ContentProvider.getUriWithoutUserId(uri), pid, uid, modeFlags,
+ resolveUserId(uri));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
@NonNull
@Override
public int[] checkUriPermissions(@NonNull List<Uri> uris, int pid, int uid,
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 260e985..b5d88e8 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -274,6 +274,7 @@
int getProcessLimit();
int checkUriPermission(in Uri uri, int pid, int uid, int mode, int userId,
in IBinder callerToken);
+ int checkContentUriPermissionFull(in Uri uri, int pid, int uid, int mode, int userId);
int[] checkUriPermissions(in List<Uri> uris, int pid, int uid, int mode, int userId,
in IBinder callerToken);
void grantUriPermission(in IApplicationThread caller, in String targetPkg, in Uri uri,
@@ -715,7 +716,7 @@
* @param listener A listener to for the callback upon completion of startup data collection.
* @param userId The userId in the multi-user environment.
*/
- void setApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener,
+ void addApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener,
int userId);
@@ -724,7 +725,8 @@
*
* @param userId The userId in the multi-user environment.
*/
- void clearApplicationStartInfoCompleteListener(int userId);
+ void removeApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener,
+ int userId);
/**
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 9cf732a..d755413 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -31,6 +31,7 @@
import android.app.blob.BlobStoreManagerFrameworkInitializer;
import android.app.contentsuggestions.ContentSuggestionsManager;
import android.app.contentsuggestions.IContentSuggestionsManager;
+import android.app.ecm.EnhancedConfirmationFrameworkInitializer;
import android.app.job.JobSchedulerFrameworkInitializer;
import android.app.people.PeopleManager;
import android.app.prediction.AppPredictionManager;
@@ -1631,6 +1632,9 @@
OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers();
DeviceLockFrameworkInitializer.registerServiceWrappers();
VirtualizationFrameworkInitializer.registerServiceWrappers();
+ if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) {
+ EnhancedConfirmationFrameworkInitializer.registerServiceWrappers();
+ }
} finally {
// If any of the above code throws, we're in a pretty bad shape and the process
// will likely crash, but we'll reset it just in case there's an exception handler...
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index b303ea6..4fc25fd 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -13,3 +13,10 @@
description: "API to get importance of UID that's binding to the caller"
bug: "292533010"
}
+
+flag {
+ namespace: "backstage_power"
+ name: "app_restrictions_api"
+ description: "API to track and query restrictions applied to apps"
+ bug: "320150834"
+}
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 12229b1..6eab363 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -35,6 +35,9 @@
import android.hardware.input.VirtualMouseConfig;
import android.hardware.input.VirtualMouseRelativeEvent;
import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualStylusButtonEvent;
+import android.hardware.input.VirtualStylusConfig;
+import android.hardware.input.VirtualStylusMotionEvent;
import android.hardware.input.VirtualTouchEvent;
import android.hardware.input.VirtualTouchscreenConfig;
import android.hardware.input.VirtualNavigationTouchpadConfig;
@@ -144,6 +147,12 @@
void createVirtualNavigationTouchpad(in VirtualNavigationTouchpadConfig config, IBinder token);
/**
+ * Creates a new stylus and registers it with the input framework with the given token.
+ */
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+ void createVirtualStylus(in VirtualStylusConfig config, IBinder token);
+
+ /**
* Removes the input device corresponding to the given token from the framework.
*/
@EnforcePermission("CREATE_VIRTUAL_DEVICE")
@@ -156,32 +165,32 @@
int getInputDeviceId(IBinder token);
/**
- * Injects a key event to the virtual dpad corresponding to the given token.
- */
+ * Injects a key event to the virtual dpad corresponding to the given token.
+ */
@EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
/**
- * Injects a key event to the virtual keyboard corresponding to the given token.
- */
+ * Injects a key event to the virtual keyboard corresponding to the given token.
+ */
@EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
/**
- * Injects a button event to the virtual mouse corresponding to the given token.
- */
+ * Injects a button event to the virtual mouse corresponding to the given token.
+ */
@EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
/**
- * Injects a relative event to the virtual mouse corresponding to the given token.
- */
+ * Injects a relative event to the virtual mouse corresponding to the given token.
+ */
@EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event);
/**
- * Injects a scroll event to the virtual mouse corresponding to the given token.
- */
+ * Injects a scroll event to the virtual mouse corresponding to the given token.
+ */
@EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event);
@@ -192,6 +201,18 @@
boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event);
/**
+ * Injects a motion event from the virtual stylus input device corresponding to the given token.
+ */
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+ boolean sendStylusMotionEvent(IBinder token, in VirtualStylusMotionEvent event);
+
+ /**
+ * Injects a button event from the virtual stylus input device corresponding to the given token.
+ */
+ @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+ boolean sendStylusButtonEvent(IBinder token, in VirtualStylusButtonEvent event);
+
+ /**
* Returns all virtual sensors created for this device.
*/
@EnforcePermission("CREATE_VIRTUAL_DEVICE")
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 2abeeee..c1e443d 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -42,6 +42,8 @@
import android.hardware.input.VirtualMouseConfig;
import android.hardware.input.VirtualNavigationTouchpad;
import android.hardware.input.VirtualNavigationTouchpadConfig;
+import android.hardware.input.VirtualStylus;
+import android.hardware.input.VirtualStylusConfig;
import android.hardware.input.VirtualTouchscreen;
import android.hardware.input.VirtualTouchscreenConfig;
import android.media.AudioManager;
@@ -316,6 +318,19 @@
}
}
+ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ @NonNull
+ VirtualStylus createVirtualStylus(@NonNull VirtualStylusConfig config) {
+ try {
+ final IBinder token = new Binder(
+ "android.hardware.input.VirtualStylus:" + config.getInputDeviceName());
+ mVirtualDevice.createVirtualStylus(config, token);
+ return new VirtualStylus(config, mVirtualDevice, token);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
@NonNull
VirtualNavigationTouchpad createVirtualNavigationTouchpad(
@NonNull VirtualNavigationTouchpadConfig config) {
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index eef60f1..a4cada2 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -55,6 +55,8 @@
import android.hardware.input.VirtualMouseConfig;
import android.hardware.input.VirtualNavigationTouchpad;
import android.hardware.input.VirtualNavigationTouchpadConfig;
+import android.hardware.input.VirtualStylus;
+import android.hardware.input.VirtualStylusConfig;
import android.hardware.input.VirtualTouchscreen;
import android.hardware.input.VirtualTouchscreenConfig;
import android.media.AudioManager;
@@ -859,6 +861,19 @@
}
/**
+ * Creates a virtual stylus.
+ *
+ * @param config the touchscreen configurations for the virtual stylus.
+ */
+ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ @NonNull
+ @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+ public VirtualStylus createVirtualStylus(
+ @NonNull VirtualStylusConfig config) {
+ return mVirtualDeviceInternal.createVirtualStylus(config);
+ }
+
+ /**
* Creates a VirtualAudioDevice, capable of recording audio emanating from this device,
* or injecting audio from another device.
*
@@ -886,11 +901,14 @@
}
/**
- * Creates a new virtual camera. If a virtual camera was already created, it will be closed.
+ * Creates a new virtual camera with the given {@link VirtualCameraConfig}. A virtual device
+ * can create a virtual camera only if it has
+ * {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM} as its
+ * {@link VirtualDeviceParams#POLICY_TYPE_CAMERA}.
*
- * @param config camera config.
- * @return newly created camera;
- * @hide
+ * @param config camera configuration.
+ * @return newly created camera.
+ * @see VirtualDeviceParams#POLICY_TYPE_CAMERA
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 0253ddd..f9a9da1 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -159,7 +159,7 @@
* @hide
*/
@IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO,
- POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY})
+ POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
public @interface PolicyType {}
@@ -246,6 +246,23 @@
@FlaggedApi(Flags.FLAG_CROSS_DEVICE_CLIPBOARD)
public static final int POLICY_TYPE_CLIPBOARD = 4;
+ /**
+ * Tells the camera framework how to handle camera requests for the front and back cameras from
+ * contexts associated with this virtual device.
+ *
+ * <ul>
+ * <li>{@link #DEVICE_POLICY_DEFAULT}: Returns the front and back cameras of the default
+ * device.
+ * <li>{@link #DEVICE_POLICY_CUSTOM}: Returns the front and back cameras cameras of the
+ * virtual device. Note that if the virtual device did not create any virtual cameras,
+ * then no front and back cameras will be available.
+ * </ul>
+ *
+ * @see Context#getDeviceId
+ */
+ @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+ public static final int POLICY_TYPE_CAMERA = 5;
+
private final int mLockState;
@NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts;
@NavigationPolicy
@@ -1153,6 +1170,10 @@
mDevicePolicies.delete(POLICY_TYPE_CLIPBOARD);
}
+ if (!Flags.virtualCamera()) {
+ mDevicePolicies.delete(POLICY_TYPE_CAMERA);
+ }
+
if ((mAudioPlaybackSessionId != AUDIO_SESSION_ID_GENERATE
|| mAudioRecordingSessionId != AUDIO_SESSION_ID_GENERATE)
&& mDevicePolicies.get(POLICY_TYPE_AUDIO, DEVICE_POLICY_DEFAULT)
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
index 44942d6..f4f69b5 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
+++ b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package android.companion.virtual.camera;
-import android.companion.virtual.camera.VirtualCameraStreamConfig;
import android.view.Surface;
/**
@@ -30,38 +30,36 @@
* Called when one of the requested stream has been configured by the virtual camera service and
* is ready to receive data onto its {@link Surface}
*
- * @param streamId The id of the configured stream
- * @param surface The surface to write data into for this stream
- * @param streamConfig The image data configuration for this stream
+ * @param streamId The id of the configured stream
+ * @param surface The surface to write data into for this stream
+ * @param width The width of the surface
+ * @param height The height of the surface
+ * @param format The pixel format of the surface
*/
- oneway void onStreamConfigured(
- int streamId,
- in Surface surface,
- in VirtualCameraStreamConfig streamConfig);
+ oneway void onStreamConfigured(int streamId, in Surface surface, int width, int height,
+ int format);
/**
* The client application is requesting a camera frame for the given streamId and frameId.
*
* <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
- * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
- * VirtualCameraStreamConfig)} call.
+ * this stream that was provided during the
+ * {@link #onStreamConfigured(int, Surface, int, int, int)} call.
*
* @param streamId The streamId for which the frame is requested. This corresponds to the
- * streamId that was given in {@link #onStreamConfigured(int, Surface,
- * VirtualCameraStreamConfig)}
+ * streamId that was given in {@link #onStreamConfigured(int, Surface, int, int, int)}
* @param frameId The frameId that is being requested. Each request will have a different
* frameId, that will be increasing for each call with a particular streamId.
*/
oneway void onProcessCaptureRequest(int streamId, long frameId);
/**
- * The stream previously configured when {@link #onStreamConfigured(int, Surface,
- * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
- * freed. The Surface was disposed on the client side and should not be used anymore by the
- * virtual camera owner.
+ * The stream previously configured when
+ * {@link #onStreamConfigured(int, Surface, int, int, int)} was called is now being closed and
+ * associated resources can be freed. The Surface was disposed on the client side and should not
+ * be used anymore by the virtual camera owner.
*
* @param streamId The id of the stream that was closed.
*/
oneway void onStreamClosed(int streamId);
-
}
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
index 5b658b8..c894de4 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
@@ -17,9 +17,11 @@
package android.companion.virtual.camera;
import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.companion.virtual.flags.Flags;
+import android.graphics.ImageFormat;
import android.view.Surface;
import java.util.concurrent.Executor;
@@ -41,33 +43,33 @@
*
* @param streamId The id of the configured stream
* @param surface The surface to write data into for this stream
- * @param streamConfig The image data configuration for this stream
+ * @param width The width of the surface
+ * @param height The height of the surface
+ * @param format The {@link ImageFormat} of the surface
*/
- void onStreamConfigured(
- int streamId,
- @NonNull Surface surface,
- @NonNull VirtualCameraStreamConfig streamConfig);
+ void onStreamConfigured(int streamId, @NonNull Surface surface,
+ @IntRange(from = 1) int width, @IntRange(from = 1) int height,
+ @ImageFormat.Format int format);
/**
* The client application is requesting a camera frame for the given streamId and frameId.
*
* <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
- * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
- * VirtualCameraStreamConfig)} call.
+ * this stream that was provided during the
+ * {@link #onStreamConfigured(int, Surface, int, int, int)} call.
*
* @param streamId The streamId for which the frame is requested. This corresponds to the
- * streamId that was given in {@link #onStreamConfigured(int, Surface,
- * VirtualCameraStreamConfig)}
+ * streamId that was given in {@link #onStreamConfigured(int, Surface, int, int, int)}
* @param frameId The frameId that is being requested. Each request will have a different
* frameId, that will be increasing for each call with a particular streamId.
*/
default void onProcessCaptureRequest(int streamId, long frameId) {}
/**
- * The stream previously configured when {@link #onStreamConfigured(int, Surface,
- * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
- * freed. The Surface corresponding to that streamId was disposed on the client side and should
- * not be used anymore by the virtual camera owner
+ * The stream previously configured when
+ * {@link #onStreamConfigured(int, Surface, int, int, int)} was called is now being closed and
+ * associated resources can be freed. The Surface corresponding to that streamId was disposed on
+ * the client side and should not be used anymore by the virtual camera owner.
*
* @param streamId The id of the stream that was closed.
*/
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
index 88c27a5..5ceb4d1 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 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.
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package android.companion.virtual.camera;
/** @hide */
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index 59fe9a1..350cf3d 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -19,16 +19,23 @@
import static java.util.Objects.requireNonNull;
import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
+import android.companion.virtual.VirtualDevice;
import android.companion.virtual.flags.Flags;
import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.hardware.camera2.CameraMetadata;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArraySet;
import android.view.Surface;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -43,16 +50,57 @@
@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public final class VirtualCameraConfig implements Parcelable {
+ private static final int LENS_FACING_UNKNOWN = -1;
+
+ /**
+ * Sensor orientation of {@code 0} degrees.
+ * @see #getSensorOrientation
+ */
+ public static final int SENSOR_ORIENTATION_0 = 0;
+ /**
+ * Sensor orientation of {@code 90} degrees.
+ * @see #getSensorOrientation
+ */
+ public static final int SENSOR_ORIENTATION_90 = 90;
+ /**
+ * Sensor orientation of {@code 180} degrees.
+ * @see #getSensorOrientation
+ */
+ public static final int SENSOR_ORIENTATION_180 = 180;
+ /**
+ * Sensor orientation of {@code 270} degrees.
+ * @see #getSensorOrientation
+ */
+ public static final int SENSOR_ORIENTATION_270 = 270;
+ /** @hide */
+ @IntDef(prefix = {"SENSOR_ORIENTATION_"}, value = {
+ SENSOR_ORIENTATION_0,
+ SENSOR_ORIENTATION_90,
+ SENSOR_ORIENTATION_180,
+ SENSOR_ORIENTATION_270
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SensorOrientation {}
+
private final String mName;
private final Set<VirtualCameraStreamConfig> mStreamConfigurations;
private final IVirtualCameraCallback mCallback;
+ @SensorOrientation
+ private final int mSensorOrientation;
+ private final int mLensFacing;
private VirtualCameraConfig(
@NonNull String name,
@NonNull Set<VirtualCameraStreamConfig> streamConfigurations,
@NonNull Executor executor,
- @NonNull VirtualCameraCallback callback) {
+ @NonNull VirtualCameraCallback callback,
+ @SensorOrientation int sensorOrientation,
+ int lensFacing) {
mName = requireNonNull(name, "Missing name");
+ if (lensFacing == LENS_FACING_UNKNOWN) {
+ throw new IllegalArgumentException("Lens facing must be set");
+ }
+ mLensFacing = lensFacing;
mStreamConfigurations =
Set.copyOf(requireNonNull(streamConfigurations, "Missing stream configurations"));
if (mStreamConfigurations.isEmpty()) {
@@ -63,6 +111,7 @@
new VirtualCameraCallbackInternal(
requireNonNull(callback, "Missing callback"),
requireNonNull(executor, "Missing callback executor"));
+ mSensorOrientation = sensorOrientation;
}
private VirtualCameraConfig(@NonNull Parcel in) {
@@ -73,6 +122,8 @@
in.readParcelableArray(
VirtualCameraStreamConfig.class.getClassLoader(),
VirtualCameraStreamConfig.class));
+ mSensorOrientation = in.readInt();
+ mLensFacing = in.readInt();
}
@Override
@@ -86,6 +137,8 @@
dest.writeStrongInterface(mCallback);
dest.writeParcelableArray(
mStreamConfigurations.toArray(new VirtualCameraStreamConfig[0]), flags);
+ dest.writeInt(mSensorOrientation);
+ dest.writeInt(mLensFacing);
}
/**
@@ -100,7 +153,7 @@
* Returns an unmodifiable set of the stream configurations added to this {@link
* VirtualCameraConfig}.
*
- * @see VirtualCameraConfig.Builder#addStreamConfig(int, int, int)
+ * @see VirtualCameraConfig.Builder#addStreamConfig(int, int, int, int)
*/
@NonNull
public Set<VirtualCameraStreamConfig> getStreamConfigs() {
@@ -118,13 +171,33 @@
}
/**
+ * Returns the sensor orientation of this stream, which represents the clockwise angle (in
+ * degrees) through which the output image needs to be rotated to be upright on the device
+ * screen in its native orientation. Returns {@link #SENSOR_ORIENTATION_0} if omitted.
+ */
+ @SensorOrientation
+ public int getSensorOrientation() {
+ return mSensorOrientation;
+ }
+
+ /**
+ * Returns the direction that the virtual camera faces relative to the virtual device's screen.
+ *
+ * @see Builder#setLensFacing(int)
+ */
+ public int getLensFacing() {
+ return mLensFacing;
+ }
+
+ /**
* Builder for {@link VirtualCameraConfig}.
*
* <p>To build an instance of {@link VirtualCameraConfig} the following conditions must be met:
- * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int)}.
+ * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int, int)}.
* <li>A callback must be set with {@link #setVirtualCameraCallback(Executor,
* VirtualCameraCallback)}
* <li>A camera name must be set with {@link #setName(String)}
+ * <li>A lens facing must be set with {@link #setLensFacing(int)}
*/
@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public static final class Builder {
@@ -133,9 +206,11 @@
private final ArraySet<VirtualCameraStreamConfig> mStreamConfigurations = new ArraySet<>();
private Executor mCallbackExecutor;
private VirtualCameraCallback mCallback;
+ private int mSensorOrientation = SENSOR_ORIENTATION_0;
+ private int mLensFacing = LENS_FACING_UNKNOWN;
/**
- * Set the name of the virtual camera instance.
+ * Sets the name of the virtual camera instance.
*/
@NonNull
public Builder setName(@NonNull String name) {
@@ -144,25 +219,94 @@
}
/**
- * Add an available stream configuration fot this {@link VirtualCamera}.
+ * Adds a supported input stream configuration for this {@link VirtualCamera}.
*
* <p>At least one {@link VirtualCameraStreamConfig} must be added.
*
* @param width The width of the stream.
* @param height The height of the stream.
- * @param format The {@link ImageFormat} of the stream.
+ * @param format The input format of the stream. Supported formats are
+ * {@link ImageFormat#YUV_420_888} and {@link PixelFormat#RGBA_8888}.
+ * @param maximumFramesPerSecond The maximum frame rate (in frames per second) for the
+ * stream.
*
- * @throws IllegalArgumentException if invalid format or dimensions are passed.
+ * @throws IllegalArgumentException if invalid dimensions, format or frame rate are passed.
*/
@NonNull
- public Builder addStreamConfig(int width, int height, @ImageFormat.Format int format) {
- if (width <= 0 || height <= 0) {
- throw new IllegalArgumentException("Invalid dimensions passed for stream config");
+ public Builder addStreamConfig(
+ @IntRange(from = 1) int width,
+ @IntRange(from = 1) int height,
+ @ImageFormat.Format int format,
+ @IntRange(from = 1) int maximumFramesPerSecond) {
+ // TODO(b/310857519): Check dimension upper limits based on the maximum texture size
+ // supported by the current device, instead of hardcoded limits.
+ if (width <= 0 || width > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) {
+ throw new IllegalArgumentException(
+ "Invalid width passed for stream config: " + width
+ + ", must be between 1 and "
+ + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT);
}
- if (!ImageFormat.isPublicFormat(format)) {
- throw new IllegalArgumentException("Invalid format passed for stream config");
+ if (height <= 0 || height > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) {
+ throw new IllegalArgumentException(
+ "Invalid height passed for stream config: " + height
+ + ", must be between 1 and "
+ + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT);
}
- mStreamConfigurations.add(new VirtualCameraStreamConfig(width, height, format));
+ if (!isFormatSupported(format)) {
+ throw new IllegalArgumentException(
+ "Invalid format passed for stream config: " + format);
+ }
+ if (maximumFramesPerSecond <= 0
+ || maximumFramesPerSecond > VirtualCameraStreamConfig.MAX_FPS_UPPER_LIMIT) {
+ throw new IllegalArgumentException(
+ "Invalid maximumFramesPerSecond, must be greater than 0 and less than "
+ + VirtualCameraStreamConfig.MAX_FPS_UPPER_LIMIT);
+ }
+ mStreamConfigurations.add(new VirtualCameraStreamConfig(width, height, format,
+ maximumFramesPerSecond));
+ return this;
+ }
+
+ /**
+ * Sets the sensor orientation of the virtual camera. This field is optional and can be
+ * omitted (defaults to {@link #SENSOR_ORIENTATION_0}).
+ *
+ * @param sensorOrientation The sensor orientation of the camera, which represents the
+ * clockwise angle (in degrees) through which the output image
+ * needs to be rotated to be upright on the device screen in its
+ * native orientation.
+ */
+ @NonNull
+ public Builder setSensorOrientation(@SensorOrientation int sensorOrientation) {
+ if (sensorOrientation != SENSOR_ORIENTATION_0
+ && sensorOrientation != SENSOR_ORIENTATION_90
+ && sensorOrientation != SENSOR_ORIENTATION_180
+ && sensorOrientation != SENSOR_ORIENTATION_270) {
+ throw new IllegalArgumentException(
+ "Invalid sensor orientation: " + sensorOrientation);
+ }
+ mSensorOrientation = sensorOrientation;
+ return this;
+ }
+
+ /**
+ * Sets the lens facing direction of the virtual camera, can be either
+ * {@link CameraMetadata#LENS_FACING_FRONT} or {@link CameraMetadata#LENS_FACING_BACK}.
+ *
+ * <p>A {@link VirtualDevice} can have at most one {@link VirtualCamera} with
+ * {@link CameraMetadata#LENS_FACING_FRONT} and at most one {@link VirtualCamera} with
+ * {@link CameraMetadata#LENS_FACING_BACK}.
+ *
+ * @param lensFacing The direction that the virtual camera faces relative to the device's
+ * screen.
+ */
+ @NonNull
+ public Builder setLensFacing(int lensFacing) {
+ if (lensFacing != CameraMetadata.LENS_FACING_BACK
+ && lensFacing != CameraMetadata.LENS_FACING_FRONT) {
+ throw new IllegalArgumentException("Unsupported lens facing: " + lensFacing);
+ }
+ mLensFacing = lensFacing;
return this;
}
@@ -189,11 +333,13 @@
* Builds a new instance of {@link VirtualCameraConfig}
*
* @throws NullPointerException if some required parameters are missing.
+ * @throws IllegalArgumentException if any parameter is invalid.
*/
@NonNull
public VirtualCameraConfig build() {
return new VirtualCameraConfig(
- mName, mStreamConfigurations, mCallbackExecutor, mCallback);
+ mName, mStreamConfigurations, mCallbackExecutor, mCallback, mSensorOrientation,
+ mLensFacing);
}
}
@@ -208,9 +354,10 @@
}
@Override
- public void onStreamConfigured(
- int streamId, Surface surface, VirtualCameraStreamConfig streamConfig) {
- mExecutor.execute(() -> mCallback.onStreamConfigured(streamId, surface, streamConfig));
+ public void onStreamConfigured(int streamId, Surface surface, int width, int height,
+ int format) {
+ mExecutor.execute(() -> mCallback.onStreamConfigured(streamId, surface, width, height,
+ format));
}
@Override
@@ -237,4 +384,11 @@
return new VirtualCameraConfig[size];
}
};
+
+ private static boolean isFormatSupported(@ImageFormat.Format int format) {
+ return switch (format) {
+ case ImageFormat.YUV_420_888, PixelFormat.RGBA_8888 -> true;
+ default -> false;
+ };
+ }
}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
index 1272f16..00a814e 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 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.
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package android.companion.virtual.camera;
import android.annotation.FlaggedApi;
@@ -24,6 +25,8 @@
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.Objects;
/**
@@ -34,32 +37,47 @@
@SystemApi
@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public final class VirtualCameraStreamConfig implements Parcelable {
+ // TODO(b/310857519): Check if we should increase the fps upper limit in future.
+ static final int MAX_FPS_UPPER_LIMIT = 60;
+ // This is the minimum guaranteed upper bound of texture size supported by all devices.
+ // Keep this in sync with kMaxTextureSize from services/camera/virtualcamera/util/Util.cc
+ // TODO(b/310857519): Remove this once we add support for fetching the maximum texture size
+ // supported by the current device.
+ static final int DIMENSION_UPPER_LIMIT = 2048;
private final int mWidth;
private final int mHeight;
private final int mFormat;
+ private final int mMaxFps;
/**
* Construct a new instance of {@link VirtualCameraStreamConfig} initialized with the provided
- * width, height and {@link ImageFormat}
+ * width, height and {@link ImageFormat}.
*
* @param width The width of the stream.
* @param height The height of the stream.
* @param format The {@link ImageFormat} of the stream.
+ * @param maxFps The maximum frame rate (in frames per second) for the stream.
+ *
+ * @hide
*/
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public VirtualCameraStreamConfig(
@IntRange(from = 1) int width,
@IntRange(from = 1) int height,
- @ImageFormat.Format int format) {
+ @ImageFormat.Format int format,
+ @IntRange(from = 1) int maxFps) {
this.mWidth = width;
this.mHeight = height;
this.mFormat = format;
+ this.mMaxFps = maxFps;
}
private VirtualCameraStreamConfig(@NonNull Parcel in) {
mWidth = in.readInt();
mHeight = in.readInt();
mFormat = in.readInt();
+ mMaxFps = in.readInt();
}
@Override
@@ -72,6 +90,7 @@
dest.writeInt(mWidth);
dest.writeInt(mHeight);
dest.writeInt(mFormat);
+ dest.writeInt(mMaxFps);
}
@NonNull
@@ -105,12 +124,13 @@
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VirtualCameraStreamConfig that = (VirtualCameraStreamConfig) o;
- return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat;
+ return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat
+ && mMaxFps == that.mMaxFps;
}
@Override
public int hashCode() {
- return Objects.hash(mWidth, mHeight, mFormat);
+ return Objects.hash(mWidth, mHeight, mFormat, mMaxFps);
}
/** Returns the {@link ImageFormat} of this stream. */
@@ -118,4 +138,10 @@
public int getFormat() {
return mFormat;
}
+
+ /** Returns the maximum frame rate (in frames per second) of this stream. */
+ @IntRange(from = 1)
+ public int getMaximumFramesPerSecond() {
+ return mMaxFps;
+ }
}
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index ce2490b..588e4fc 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -100,3 +100,10 @@
description: "Enable interactive screen mirroring using Virtual Devices"
bug: "292212199"
}
+
+flag {
+ name: "virtual_stylus"
+ namespace: "virtual_devices"
+ description: "Enable virtual stylus input"
+ bug: "304829446"
+}
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index a1357c9..bde562d 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -231,7 +231,7 @@
}
/** @hide */
- public AttributionSource withToken(@NonNull Binder token) {
+ public AttributionSource withToken(@NonNull IBinder token) {
return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(),
token, mAttributionSourceState.renouncedPermissions, getDeviceId(), getNext());
}
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index e9b94c9..c7a75ed 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -41,7 +41,6 @@
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.SQLException;
-import android.multiuser.Flags;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Binder;
@@ -147,7 +146,6 @@
private boolean mExported;
private boolean mNoPerms;
private boolean mSingleUser;
- private boolean mSystemUserOnly;
private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray();
private ThreadLocal<AttributionSource> mCallingAttributionSource;
@@ -379,9 +377,7 @@
!= PermissionChecker.PERMISSION_GRANTED
&& getContext().checkUriPermission(userUri, Binder.getCallingPid(),
callingUid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
- != PackageManager.PERMISSION_GRANTED
- && !deniedAccessSystemUserOnlyProvider(callingUserId,
- mSystemUserOnly)) {
+ != PackageManager.PERMISSION_GRANTED) {
FrameworkStatsLog.write(GET_TYPE_ACCESSED_WITHOUT_PERMISSION,
enumCheckUriPermission,
callingUid, uri.getAuthority(), type);
@@ -869,10 +865,6 @@
boolean checkUser(int pid, int uid, Context context) {
final int callingUserId = UserHandle.getUserId(uid);
- if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
- return false;
- }
-
if (callingUserId == context.getUserId() || mSingleUser) {
return true;
}
@@ -995,9 +987,6 @@
// last chance, check against any uri grants
final int callingUserId = UserHandle.getUserId(uid);
- if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
- return PermissionChecker.PERMISSION_HARD_DENIED;
- }
final Uri userUri = (mSingleUser && !UserHandle.isSameUser(mMyUid, uid))
? maybeAddUserId(uri, callingUserId) : uri;
if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
@@ -2634,7 +2623,6 @@
setPathPermissions(info.pathPermissions);
mExported = info.exported;
mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0;
- mSystemUserOnly = (info.flags & ProviderInfo.FLAG_SYSTEM_USER_ONLY) != 0;
setAuthorities(info.authority);
}
if (Build.IS_DEBUGGABLE) {
@@ -2768,11 +2756,6 @@
String auth = uri.getAuthority();
if (!mSingleUser) {
int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT);
- if (deniedAccessSystemUserOnlyProvider(mContext.getUserId(),
- mSystemUserOnly)) {
- throw new SecurityException("Trying to query a SYSTEM user only content"
- + " provider from user:" + mContext.getUserId());
- }
if (userId != UserHandle.USER_CURRENT
&& userId != mContext.getUserId()
// Since userId specified in content uri, the provider userId would be
@@ -2946,16 +2929,4 @@
Trace.traceBegin(traceTag, methodName + subInfo);
}
}
- /**
- * Return true if access to content provider is denied because it's a SYSTEM user only
- * provider and the calling user is not the SYSTEM user.
- *
- * @param callingUserId UserId of the caller accessing the content provider.
- * @param systemUserOnly true when the content provider is only available for the SYSTEM user.
- */
- private static boolean deniedAccessSystemUserOnlyProvider(int callingUserId,
- boolean systemUserOnly) {
- return Flags.enableSystemUserOnlyForServicesAndProviders()
- && (callingUserId != UserHandle.USER_SYSTEM && systemUserOnly);
- }
}
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index fa76e39..249c0e43 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -16,6 +16,8 @@
package android.content;
+import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS;
+
import android.annotation.AttrRes;
import android.annotation.CallbackExecutor;
import android.annotation.CheckResult;
@@ -296,6 +298,7 @@
BIND_ALLOW_ACTIVITY_STARTS,
BIND_INCLUDE_CAPABILITIES,
BIND_SHARED_ISOLATED_PROCESS,
+ BIND_PACKAGE_ISOLATED_PROCESS,
BIND_EXTERNAL_SERVICE
})
@Retention(RetentionPolicy.SOURCE)
@@ -318,6 +321,7 @@
BIND_ALLOW_ACTIVITY_STARTS,
BIND_INCLUDE_CAPABILITIES,
BIND_SHARED_ISOLATED_PROCESS,
+ BIND_PACKAGE_ISOLATED_PROCESS,
// Intentionally not include BIND_EXTERNAL_SERVICE, because it'd cause sign-extension.
// This would allow Android Studio to show a warning, if someone tries to use
// BIND_EXTERNAL_SERVICE BindServiceFlags.
@@ -511,6 +515,26 @@
*/
public static final int BIND_SHARED_ISOLATED_PROCESS = 0x00002000;
+ /**
+ * Flag for {@link #bindIsolatedService}: Bind the service into a shared isolated process,
+ * but only with other isolated services from the same package that declare the same process
+ * name.
+ *
+ * <p>Specifying this flag allows multiple isolated services defined in the same package to be
+ * running in a single shared isolated process. This shared isolated process must be specified
+ * since this flag will not work with the default application process.
+ *
+ * <p>This flag is different from {@link #BIND_SHARED_ISOLATED_PROCESS} since it only
+ * allows binding services from the same package in the same shared isolated process. This also
+ * means the shared package isolated process is global, and not scoped to each potential
+ * calling app.
+ *
+ * <p>The shared isolated process instance is identified by the "android:process" attribute
+ * defined by the service. This flag cannot be used without this attribute set.
+ */
+ @FlaggedApi(FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS)
+ public static final int BIND_PACKAGE_ISOLATED_PROCESS = 1 << 14;
+
/*********** Public flags above this line ***********/
/*********** Hidden flags below this line ***********/
@@ -4218,6 +4242,7 @@
VIRTUALIZATION_SERVICE,
GRAMMATICAL_INFLECTION_SERVICE,
SECURITY_STATE_SERVICE,
+ //@hide: ECM_ENHANCED_CONFIRMATION_SERVICE,
})
@Retention(RetentionPolicy.SOURCE)
@@ -6503,6 +6528,18 @@
public static final String SECURITY_STATE_SERVICE = "security_state";
/**
+ * Use with {@link #getSystemService(String)} to retrieve an
+ * {@link android.app.ecm.EnhancedConfirmationManager}.
+ *
+ * @see #getSystemService(String)
+ * @see android.app.ecm.EnhancedConfirmationManager
+ * @hide
+ */
+ @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+ @SystemApi
+ public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation";
+
+ /**
* Determine whether the given permission is allowed for a particular
* process and user ID running in the system.
*
@@ -6734,7 +6771,7 @@
@Intent.AccessUriMode int modeFlags);
/**
- * Determine whether a particular process and user ID has been granted
+ * Determine whether a particular process and uid has been granted
* permission to access a specific URI. This only checks for permissions
* that have been explicitly granted -- if the given process/uid has
* more general access to the URI's content provider then this check will
@@ -6758,7 +6795,38 @@
@Intent.AccessUriMode int modeFlags);
/**
- * Determine whether a particular process and user ID has been granted
+ * Determine whether a particular process and uid has been granted
+ * permission to access a specific content URI.
+ *
+ * <p>Unlike {@link #checkUriPermission(Uri, int, int, int)}, this method
+ * checks for general access to the URI's content provider, as well as
+ * explicitly granted permissions.</p>
+ *
+ * <p>Note, this check will throw an {@link IllegalArgumentException}
+ * for non-content URIs.</p>
+ *
+ * @param uri The content uri that is being checked.
+ * @param pid (Optional) The process ID being checked against. If the
+ * pid is unknown, pass -1.
+ * @param uid The UID being checked against. A uid of 0 is the root
+ * user, which will pass every permission check.
+ * @param modeFlags The access modes to check.
+ *
+ * @return {@link PackageManager#PERMISSION_GRANTED} if the given
+ * pid/uid is allowed to access that uri, or
+ * {@link PackageManager#PERMISSION_DENIED} if it is not.
+ *
+ * @see #checkUriPermission(Uri, int, int, int)
+ */
+ @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS)
+ @PackageManager.PermissionResult
+ public int checkContentUriPermissionFull(@NonNull Uri uri, int pid, int uid,
+ @Intent.AccessUriMode int modeFlags) {
+ throw new RuntimeException("Not implemented. Must override in a subclass.");
+ }
+
+ /**
+ * Determine whether a particular process and uid has been granted
* permission to access a list of URIs. This only checks for permissions
* that have been explicitly granted -- if the given process/uid has
* more general access to the URI's content provider then this check will
@@ -6794,7 +6862,7 @@
@Intent.AccessUriMode int modeFlags, IBinder callerToken);
/**
- * Determine whether the calling process and user ID has been
+ * Determine whether the calling process and uid has been
* granted permission to access a specific URI. This is basically
* the same as calling {@link #checkUriPermission(Uri, int, int,
* int)} with the pid and uid returned by {@link
@@ -6817,7 +6885,7 @@
public abstract int checkCallingUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags);
/**
- * Determine whether the calling process and user ID has been
+ * Determine whether the calling process and uid has been
* granted permission to access a list of URIs. This is basically
* the same as calling {@link #checkUriPermissions(List, int, int, int)}
* with the pid and uid returned by {@link
@@ -6911,7 +6979,7 @@
@Intent.AccessUriMode int modeFlags);
/**
- * If a particular process and user ID has not been granted
+ * If a particular process and uid has not been granted
* permission to access a specific URI, throw {@link
* SecurityException}. This only checks for permissions that have
* been explicitly granted -- if the given process/uid has more
@@ -6931,7 +6999,7 @@
Uri uri, int pid, int uid, @Intent.AccessUriMode int modeFlags, String message);
/**
- * If the calling process and user ID has not been granted
+ * If the calling process and uid has not been granted
* permission to access a specific URI, throw {@link
* SecurityException}. This is basically the same as calling
* {@link #enforceUriPermission(Uri, int, int, int, String)} with
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 0a8029c..e0cf0a5 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -17,6 +17,7 @@
package android.content;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -1003,6 +1004,12 @@
return mBase.checkUriPermission(uri, pid, uid, modeFlags);
}
+ @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS)
+ @Override
+ public int checkContentUriPermissionFull(@NonNull Uri uri, int pid, int uid, int modeFlags) {
+ return mBase.checkContentUriPermissionFull(uri, pid, uid, modeFlags);
+ }
+
@NonNull
@Override
public int[] checkUriPermissions(@NonNull List<Uri> uris, int pid, int uid,
diff --git a/core/java/android/content/flags/flags.aconfig b/core/java/android/content/flags/flags.aconfig
new file mode 100644
index 0000000..3445fb5
--- /dev/null
+++ b/core/java/android/content/flags/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.content.flags"
+
+flag {
+ name: "enable_bind_package_isolated_process"
+ namespace: "machine_learning"
+ description: "This flag enables the newly added flag for binding package-private isolated processes."
+ bug: "312706530"
+}
\ No newline at end of file
diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java
index de33fa8..9e553db 100644
--- a/core/java/android/content/pm/ProviderInfo.java
+++ b/core/java/android/content/pm/ProviderInfo.java
@@ -89,15 +89,6 @@
public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
/**
- * Bit in {@link #flags}: If set, this provider will only be available
- * for the system user.
- * Set from the android.R.attr#systemUserOnly attribute.
- * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
- * @hide
- */
- public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
-
- /**
* Bit in {@link #flags}: If set, a single instance of the provider will
* run for all users on the device. Set from the
* {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 2b378b1..ae46c027 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -101,14 +101,6 @@
public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
/**
- * @hide Bit in {@link #flags}: If set, this service will only be available
- * for the system user.
- * Set from the android.R.attr#systemUserOnly attribute.
- * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
- */
- public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
-
- /**
* Bit in {@link #flags}: If set, a single instance of the service will
* run for all users on the device. Set from the
* {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 57749d4..269c6c2 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -123,6 +123,7 @@
* @hide
*/
@IntDef(prefix = "SHOW_IN_LAUNCHER_", value = {
+ SHOW_IN_LAUNCHER_UNKNOWN,
SHOW_IN_LAUNCHER_WITH_PARENT,
SHOW_IN_LAUNCHER_SEPARATE,
SHOW_IN_LAUNCHER_NO,
@@ -131,6 +132,13 @@
public @interface ShowInLauncher {
}
/**
+ * Indicates that the show in launcher value for this profile is unknown or unsupported.
+ * @hide
+ */
+ @TestApi
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int SHOW_IN_LAUNCHER_UNKNOWN = -1;
+ /**
* Suggests that the launcher should show this user's apps in the main tab.
* That is, either this user is a full user, so its apps should be presented accordingly, or, if
* this user is a profile, then its apps should be shown alongside its parent's apps.
@@ -157,6 +165,7 @@
* @hide
*/
@IntDef(prefix = "SHOW_IN_SETTINGS_", value = {
+ SHOW_IN_SETTINGS_UNKNOWN,
SHOW_IN_SETTINGS_WITH_PARENT,
SHOW_IN_SETTINGS_SEPARATE,
SHOW_IN_SETTINGS_NO,
@@ -165,6 +174,12 @@
public @interface ShowInSettings {
}
/**
+ * Indicates that the show in settings value for this profile is unknown or unsupported.
+ * @hide
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int SHOW_IN_SETTINGS_UNKNOWN = -1;
+ /**
* Suggests that the Settings app should show this user's apps in the main tab.
* That is, either this user is a full user, so its apps should be presented accordingly, or, if
* this user is a profile, then its apps should be shown alongside its parent's apps.
@@ -309,6 +324,7 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "SHOW_IN_QUIET_MODE_",
value = {
+ SHOW_IN_QUIET_MODE_UNKNOWN,
SHOW_IN_QUIET_MODE_PAUSED,
SHOW_IN_QUIET_MODE_HIDDEN,
SHOW_IN_QUIET_MODE_DEFAULT,
@@ -318,6 +334,12 @@
}
/**
+ * Indicates that the show in quiet mode value for this profile is unknown.
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int SHOW_IN_QUIET_MODE_UNKNOWN = -1;
+
+ /**
* Indicates that the profile should still be visible in quiet mode but should be shown as
* paused (e.g. by greying out its icons).
*/
@@ -347,6 +369,7 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "SHOW_IN_SHARING_SURFACES_",
value = {
+ SHOW_IN_SHARING_SURFACES_UNKNOWN,
SHOW_IN_SHARING_SURFACES_SEPARATE,
SHOW_IN_SHARING_SURFACES_WITH_PARENT,
SHOW_IN_SHARING_SURFACES_NO,
@@ -356,6 +379,12 @@
}
/**
+ * Indicates that the show in launcher value for this profile is unknown or unsupported.
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int SHOW_IN_SHARING_SURFACES_UNKNOWN = SHOW_IN_LAUNCHER_UNKNOWN;
+
+ /**
* Indicates that the profile data and apps should be shown in sharing surfaces intermixed with
* parent user's data and apps.
*/
@@ -379,7 +408,8 @@
*
* @hide
*/
- @IntDef(prefix = {"CROSS_PROFILE_CONTENT_SHARING_STRATEGY_"}, value = {
+ @IntDef(prefix = {"CROSS_PROFILE_CONTENT_SHARING_"}, value = {
+ CROSS_PROFILE_CONTENT_SHARING_UNKNOWN,
CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION,
CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
})
@@ -388,6 +418,13 @@
}
/**
+ * Signifies that cross-profile content sharing strategy, both to and from this profile, is
+ * unknown/unsupported.
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int CROSS_PROFILE_CONTENT_SHARING_UNKNOWN = -1;
+
+ /**
* Signifies that cross-profile content sharing strategy, both to and from this profile, should
* not be delegated to any other user/profile.
* For ex:
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 1036865..c7797c7 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -70,11 +70,4 @@
namespace: "profile_experiences"
description: "Add support for Private Space in resolver sheet"
bug: "307515485"
-}
-flag {
- name: "enable_system_user_only_for_services_and_providers"
- namespace: "multiuser"
- description: "Enable systemUserOnly manifest attribute for services and providers."
- bug: "302354856"
- is_fixed_read_only: true
-}
+}
\ No newline at end of file
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index f876eeb..1165f9a 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -33,4 +33,11 @@
name: "new_settings_ui"
description: "Enables new settings UI for VIC"
bug: "315209085"
+}
+
+flag {
+ namespace: "credential_manager"
+ name: "selector_ui_improvements_enabled"
+ description: "Enables Credential Selector UI improvements for VIC"
+ bug: "319448437"
}
\ No newline at end of file
diff --git a/core/java/android/hardware/biometrics/PromptContentListItem.java b/core/java/android/hardware/biometrics/PromptContentItem.java
similarity index 88%
rename from core/java/android/hardware/biometrics/PromptContentListItem.java
rename to core/java/android/hardware/biometrics/PromptContentItem.java
index fa3783d..c47b37a 100644
--- a/core/java/android/hardware/biometrics/PromptContentListItem.java
+++ b/core/java/android/hardware/biometrics/PromptContentItem.java
@@ -21,9 +21,9 @@
import android.annotation.FlaggedApi;
/**
- * A list item shown on {@link PromptVerticalListContentView}.
+ * An item shown on {@link PromptContentView}.
*/
@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
-public interface PromptContentListItem {
+public interface PromptContentItem {
}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
similarity index 75%
rename from core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java
rename to core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
index c31f8a6..c5e5a80 100644
--- a/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java
+++ b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
@@ -27,7 +27,7 @@
* A list item with bulleted text shown on {@link PromptVerticalListContentView}.
*/
@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
-public final class PromptContentListItemBulletedText implements PromptContentListItemParcelable {
+public final class PromptContentItemBulletedText implements PromptContentItemParcelable {
private final CharSequence mText;
/**
@@ -35,7 +35,7 @@
*
* @param text The text of this list item.
*/
- public PromptContentListItemBulletedText(@NonNull CharSequence text) {
+ public PromptContentItemBulletedText(@NonNull CharSequence text) {
mText = text;
}
@@ -67,15 +67,15 @@
* @see Parcelable.Creator
*/
@NonNull
- public static final Creator<PromptContentListItemBulletedText> CREATOR = new Creator<>() {
+ public static final Creator<PromptContentItemBulletedText> CREATOR = new Creator<>() {
@Override
- public PromptContentListItemBulletedText createFromParcel(Parcel in) {
- return new PromptContentListItemBulletedText(in.readCharSequence());
+ public PromptContentItemBulletedText createFromParcel(Parcel in) {
+ return new PromptContentItemBulletedText(in.readCharSequence());
}
@Override
- public PromptContentListItemBulletedText[] newArray(int size) {
- return new PromptContentListItemBulletedText[size];
+ public PromptContentItemBulletedText[] newArray(int size) {
+ return new PromptContentItemBulletedText[size];
}
};
}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java b/core/java/android/hardware/biometrics/PromptContentItemParcelable.java
similarity index 79%
rename from core/java/android/hardware/biometrics/PromptContentListItemParcelable.java
rename to core/java/android/hardware/biometrics/PromptContentItemParcelable.java
index 15271f0..668912cf 100644
--- a/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java
+++ b/core/java/android/hardware/biometrics/PromptContentItemParcelable.java
@@ -22,9 +22,9 @@
import android.os.Parcelable;
/**
- * A parcelable {@link PromptContentListItem}.
+ * A parcelable {@link PromptContentItem}.
*/
@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
-sealed interface PromptContentListItemParcelable extends PromptContentListItem, Parcelable
- permits PromptContentListItemPlainText, PromptContentListItemBulletedText {
+sealed interface PromptContentItemParcelable extends PromptContentItem, Parcelable
+ permits PromptContentItemPlainText, PromptContentItemBulletedText {
}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java
similarity index 76%
rename from core/java/android/hardware/biometrics/PromptContentListItemPlainText.java
rename to core/java/android/hardware/biometrics/PromptContentItemPlainText.java
index d72a758..6434c59 100644
--- a/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java
+++ b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java
@@ -27,7 +27,7 @@
* A list item with plain text shown on {@link PromptVerticalListContentView}.
*/
@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
-public final class PromptContentListItemPlainText implements PromptContentListItemParcelable {
+public final class PromptContentItemPlainText implements PromptContentItemParcelable {
private final CharSequence mText;
/**
@@ -35,7 +35,7 @@
*
* @param text The text of this list item.
*/
- public PromptContentListItemPlainText(@NonNull CharSequence text) {
+ public PromptContentItemPlainText(@NonNull CharSequence text) {
mText = text;
}
@@ -67,15 +67,15 @@
* @see Parcelable.Creator
*/
@NonNull
- public static final Creator<PromptContentListItemPlainText> CREATOR = new Creator<>() {
+ public static final Creator<PromptContentItemPlainText> CREATOR = new Creator<>() {
@Override
- public PromptContentListItemPlainText createFromParcel(Parcel in) {
- return new PromptContentListItemPlainText(in.readCharSequence());
+ public PromptContentItemPlainText createFromParcel(Parcel in) {
+ return new PromptContentItemPlainText(in.readCharSequence());
}
@Override
- public PromptContentListItemPlainText[] newArray(int size) {
- return new PromptContentListItemPlainText[size];
+ public PromptContentItemPlainText[] newArray(int size) {
+ return new PromptContentItemPlainText[size];
}
};
}
diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
index f3cb189..f3e6290 100644
--- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
+++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
@@ -40,9 +40,9 @@
* .setSubTitle(...)
* .setContentView(new PromptVerticalListContentView.Builder()
* .setDescription("test description")
- * .addListItem(new PromptContentListItemPlainText("test item 1"))
- * .addListItem(new PromptContentListItemPlainText("test item 2"))
- * .addListItem(new PromptContentListItemBulletedText("test item 3"))
+ * .addListItem(new PromptContentItemPlainText("test item 1"))
+ * .addListItem(new PromptContentItemPlainText("test item 2"))
+ * .addListItem(new PromptContentItemBulletedText("test item 3"))
* .build())
* .build();
* </pre>
@@ -51,11 +51,11 @@
public final class PromptVerticalListContentView implements PromptContentViewParcelable {
private static final int MAX_ITEM_NUMBER = 20;
private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;
- private final List<PromptContentListItemParcelable> mContentList;
+ private final List<PromptContentItemParcelable> mContentList;
private final CharSequence mDescription;
private PromptVerticalListContentView(
- @NonNull List<PromptContentListItemParcelable> contentList,
+ @NonNull List<PromptContentItemParcelable> contentList,
@NonNull CharSequence description) {
mContentList = contentList;
mDescription = description;
@@ -63,8 +63,8 @@
private PromptVerticalListContentView(Parcel in) {
mContentList = in.readArrayList(
- PromptContentListItemParcelable.class.getClassLoader(),
- PromptContentListItemParcelable.class);
+ PromptContentItemParcelable.class.getClassLoader(),
+ PromptContentItemParcelable.class);
mDescription = in.readCharSequence();
}
@@ -94,13 +94,13 @@
}
/**
- * Gets the list of ListItem on the content view, as set by
- * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentListItem)}.
+ * Gets the list of items on the content view, as set by
+ * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentItem)}.
*
* @return The item list on the content view.
*/
@NonNull
- public List<PromptContentListItem> getListItems() {
+ public List<PromptContentItem> getListItems() {
return new ArrayList<>(mContentList);
}
@@ -142,7 +142,7 @@
* A builder that collects arguments to be shown on the vertical list view.
*/
public static final class Builder {
- private final List<PromptContentListItemParcelable> mContentList = new ArrayList<>();
+ private final List<PromptContentItemParcelable> mContentList = new ArrayList<>();
private CharSequence mDescription;
/**
@@ -159,28 +159,50 @@
/**
* Optional: Adds a list item in the current row. Maximum {@value MAX_ITEM_NUMBER} items in
- * total.
+ * total. The maximum length for each item is {@value MAX_EACH_ITEM_CHARACTER_NUMBER}
+ * characters.
*
* @param listItem The list item view to display
* @return This builder.
*/
@NonNull
- public Builder addListItem(@NonNull PromptContentListItem listItem) {
+ public Builder addListItem(@NonNull PromptContentItem listItem) {
if (doesListItemExceedsCharLimit(listItem)) {
throw new IllegalStateException(
"The character number of list item exceeds "
+ MAX_EACH_ITEM_CHARACTER_NUMBER);
}
- mContentList.add((PromptContentListItemParcelable) listItem);
+ mContentList.add((PromptContentItemParcelable) listItem);
return this;
}
- private boolean doesListItemExceedsCharLimit(PromptContentListItem listItem) {
- if (listItem instanceof PromptContentListItemPlainText) {
- return ((PromptContentListItemPlainText) listItem).getText().length()
+
+ /**
+ * Optional: Adds a list item in the current row. Maximum {@value MAX_ITEM_NUMBER} items in
+ * total. The maximum length for each item is {@value MAX_EACH_ITEM_CHARACTER_NUMBER}
+ * characters.
+ *
+ * @param listItem The list item view to display
+ * @param index The position at which to add the item
+ * @return This builder.
+ */
+ @NonNull
+ public Builder addListItem(@NonNull PromptContentItem listItem, int index) {
+ if (doesListItemExceedsCharLimit(listItem)) {
+ throw new IllegalStateException(
+ "The character number of list item exceeds "
+ + MAX_EACH_ITEM_CHARACTER_NUMBER);
+ }
+ mContentList.add(index, (PromptContentItemParcelable) listItem);
+ return this;
+ }
+
+ private boolean doesListItemExceedsCharLimit(PromptContentItem listItem) {
+ if (listItem instanceof PromptContentItemPlainText) {
+ return ((PromptContentItemPlainText) listItem).getText().length()
> MAX_EACH_ITEM_CHARACTER_NUMBER;
- } else if (listItem instanceof PromptContentListItemBulletedText) {
- return ((PromptContentListItemBulletedText) listItem).getText().length()
+ } else if (listItem instanceof PromptContentItemBulletedText) {
+ return ((PromptContentItemBulletedText) listItem).getText().length()
> MAX_EACH_ITEM_CHARACTER_NUMBER;
} else {
return false;
diff --git a/core/java/android/hardware/input/VirtualStylus.java b/core/java/android/hardware/input/VirtualStylus.java
new file mode 100644
index 0000000..c763f740
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylus.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 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.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.companion.virtual.IVirtualDevice;
+import android.companion.virtual.flags.Flags;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * A virtual stylus which can be used to inject input into the framework that represents a stylus
+ * on a remote device.
+ *
+ * This registers an {@link android.view.InputDevice} that is interpreted like a
+ * physically-connected device and dispatches received events to it.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public class VirtualStylus extends VirtualInputDevice {
+ /** @hide */
+ public VirtualStylus(VirtualStylusConfig config, IVirtualDevice virtualDevice,
+ IBinder token) {
+ super(config, virtualDevice, token);
+ }
+
+ /**
+ * Sends a motion event to the system.
+ *
+ * @param event the event to send
+ */
+ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ public void sendMotionEvent(@NonNull VirtualStylusMotionEvent event) {
+ try {
+ if (!mVirtualDevice.sendStylusMotionEvent(mToken, event)) {
+ Log.w(TAG, "Failed to send motion event from virtual stylus "
+ + mConfig.getInputDeviceName());
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sends a button event to the system.
+ *
+ * @param event the event to send
+ */
+ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ public void sendButtonEvent(@NonNull VirtualStylusButtonEvent event) {
+ try {
+ if (!mVirtualDevice.sendStylusButtonEvent(mToken, event)) {
+ Log.w(TAG, "Failed to send button event from virtual stylus "
+ + mConfig.getInputDeviceName());
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/hardware/input/VirtualStylusButtonEvent.aidl
similarity index 72%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/hardware/input/VirtualStylusButtonEvent.aidl
index ce92b6d..7de32cc 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/hardware/input/VirtualStylusButtonEvent.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 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.
@@ -13,10 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.companion.virtual.camera;
-/**
- * The configuration of a single virtual camera stream.
- * @hide
- */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+package android.hardware.input;
+
+parcelable VirtualStylusButtonEvent;
diff --git a/core/java/android/hardware/input/VirtualStylusButtonEvent.java b/core/java/android/hardware/input/VirtualStylusButtonEvent.java
new file mode 100644
index 0000000..97a4cd0
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusButtonEvent.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2023 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.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event describing a stylus button click interaction originating from a remote device.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public final class VirtualStylusButtonEvent implements Parcelable {
+ /** @hide */
+ public static final int ACTION_UNKNOWN = -1;
+ /** Action indicating the stylus button has been pressed. */
+ public static final int ACTION_BUTTON_PRESS = MotionEvent.ACTION_BUTTON_PRESS;
+ /** Action indicating the stylus button has been released. */
+ public static final int ACTION_BUTTON_RELEASE = MotionEvent.ACTION_BUTTON_RELEASE;
+ /** @hide */
+ @IntDef(prefix = {"ACTION_"}, value = {
+ ACTION_UNKNOWN,
+ ACTION_BUTTON_PRESS,
+ ACTION_BUTTON_RELEASE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Action {}
+
+ /** @hide */
+ public static final int BUTTON_UNKNOWN = -1;
+ /** Action indicating the stylus button involved in this event is primary. */
+ public static final int BUTTON_PRIMARY = MotionEvent.BUTTON_STYLUS_PRIMARY;
+ /** Action indicating the stylus button involved in this event is secondary. */
+ public static final int BUTTON_SECONDARY = MotionEvent.BUTTON_STYLUS_SECONDARY;
+ /** @hide */
+ @IntDef(prefix = {"BUTTON_"}, value = {
+ BUTTON_UNKNOWN,
+ BUTTON_PRIMARY,
+ BUTTON_SECONDARY,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Button {}
+
+ @Action
+ private final int mAction;
+ @Button
+ private final int mButtonCode;
+ private final long mEventTimeNanos;
+
+ private VirtualStylusButtonEvent(@Action int action, @Button int buttonCode,
+ long eventTimeNanos) {
+ mAction = action;
+ mButtonCode = buttonCode;
+ mEventTimeNanos = eventTimeNanos;
+ }
+
+ private VirtualStylusButtonEvent(@NonNull Parcel parcel) {
+ mAction = parcel.readInt();
+ mButtonCode = parcel.readInt();
+ mEventTimeNanos = parcel.readLong();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) {
+ parcel.writeInt(mAction);
+ parcel.writeInt(mButtonCode);
+ parcel.writeLong(mEventTimeNanos);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Returns the button code associated with this event.
+ */
+ @Button
+ public int getButtonCode() {
+ return mButtonCode;
+ }
+
+ /**
+ * Returns the action associated with this event.
+ */
+ @Action
+ public int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Returns the time this event occurred, in the {@link SystemClock#uptimeMillis()} time base but
+ * with nanosecond (instead of millisecond) precision.
+ *
+ * @see InputEvent#getEventTime()
+ */
+ public long getEventTimeNanos() {
+ return mEventTimeNanos;
+ }
+
+ /**
+ * Builder for {@link VirtualStylusButtonEvent}.
+ */
+ @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+ public static final class Builder {
+
+ @Action
+ private int mAction = ACTION_UNKNOWN;
+ @Button
+ private int mButtonCode = BUTTON_UNKNOWN;
+ private long mEventTimeNanos = 0L;
+
+ /**
+ * Creates a {@link VirtualStylusButtonEvent} object with the current builder configuration.
+ */
+ @NonNull
+ public VirtualStylusButtonEvent build() {
+ if (mAction == ACTION_UNKNOWN) {
+ throw new IllegalArgumentException(
+ "Cannot build stylus button event with unset action");
+ }
+ if (mButtonCode == BUTTON_UNKNOWN) {
+ throw new IllegalArgumentException(
+ "Cannot build stylus button event with unset button code");
+ }
+ return new VirtualStylusButtonEvent(mAction, mButtonCode, mEventTimeNanos);
+ }
+
+ /**
+ * Sets the button code of the event.
+ *
+ * @return this builder, to allow for chaining of calls
+ */
+ @NonNull
+ public Builder setButtonCode(@Button int buttonCode) {
+ if (buttonCode != BUTTON_PRIMARY && buttonCode != BUTTON_SECONDARY) {
+ throw new IllegalArgumentException(
+ "Unsupported stylus button code : " + buttonCode);
+ }
+ mButtonCode = buttonCode;
+ return this;
+ }
+
+ /**
+ * Sets the action of the event.
+ *
+ * @return this builder, to allow for chaining of calls
+ */
+ @NonNull
+ public Builder setAction(@Action int action) {
+ if (action != ACTION_BUTTON_PRESS && action != ACTION_BUTTON_RELEASE) {
+ throw new IllegalArgumentException("Unsupported stylus button action : " + action);
+ }
+ mAction = action;
+ return this;
+ }
+
+ /**
+ * Sets the time (in nanoseconds) when this specific event was generated. This may be
+ * obtained from {@link SystemClock#uptimeMillis()} (with nanosecond precision instead of
+ * millisecond), but can be different depending on the use case.
+ * This field is optional and can be omitted.
+ *
+ * @return this builder, to allow for chaining of calls
+ * @see InputEvent#getEventTime()
+ */
+ @NonNull
+ public Builder setEventTimeNanos(long eventTimeNanos) {
+ if (eventTimeNanos < 0L) {
+ throw new IllegalArgumentException("Event time cannot be negative");
+ }
+ this.mEventTimeNanos = eventTimeNanos;
+ return this;
+ }
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<VirtualStylusButtonEvent> CREATOR =
+ new Parcelable.Creator<>() {
+ public VirtualStylusButtonEvent createFromParcel(Parcel source) {
+ return new VirtualStylusButtonEvent(source);
+ }
+
+ public VirtualStylusButtonEvent[] newArray(int size) {
+ return new VirtualStylusButtonEvent[size];
+ }
+ };
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/hardware/input/VirtualStylusConfig.aidl
similarity index 72%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/hardware/input/VirtualStylusConfig.aidl
index ce92b6d..a13eec2 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/hardware/input/VirtualStylusConfig.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 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.
@@ -13,10 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.companion.virtual.camera;
-/**
- * The configuration of a single virtual camera stream.
- * @hide
- */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+package android.hardware.input;
+
+parcelable VirtualStylusConfig;
diff --git a/core/java/android/hardware/input/VirtualStylusConfig.java b/core/java/android/hardware/input/VirtualStylusConfig.java
new file mode 100644
index 0000000..64cf1f5
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusConfig.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2023 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.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Configurations to create a virtual stylus.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public final class VirtualStylusConfig extends VirtualTouchDeviceConfig implements Parcelable {
+
+ private VirtualStylusConfig(@NonNull Builder builder) {
+ super(builder);
+ }
+
+ private VirtualStylusConfig(@NonNull Parcel in) {
+ super(in);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ }
+
+ @NonNull
+ public static final Creator<VirtualStylusConfig> CREATOR =
+ new Creator<>() {
+ @Override
+ public VirtualStylusConfig createFromParcel(Parcel in) {
+ return new VirtualStylusConfig(in);
+ }
+
+ @Override
+ public VirtualStylusConfig[] newArray(int size) {
+ return new VirtualStylusConfig[size];
+ }
+ };
+
+ /**
+ * Builder for creating a {@link VirtualStylusConfig}.
+ */
+ @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+ public static final class Builder extends VirtualTouchDeviceConfig.Builder<Builder> {
+
+ /**
+ * Creates a new instance for the given dimensions of the screen targeted by the
+ * {@link VirtualStylus}.
+ *
+ * <p>The dimensions are not pixels but in the screen's raw coordinate space. They do
+ * not necessarily have to correspond to the display size or aspect ratio. In this case the
+ * framework will handle the scaling appropriately.
+ *
+ * @param screenWidth The width of the targeted screen.
+ * @param screenHeight The height of the targeted screen.
+ */
+ public Builder(@IntRange(from = 1) int screenWidth,
+ @IntRange(from = 1) int screenHeight) {
+ super(screenWidth, screenHeight);
+ }
+
+ /**
+ * Builds the {@link VirtualStylusConfig} instance.
+ */
+ @NonNull
+ public VirtualStylusConfig build() {
+ return new VirtualStylusConfig(this);
+ }
+ }
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/hardware/input/VirtualStylusMotionEvent.aidl
similarity index 72%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/hardware/input/VirtualStylusMotionEvent.aidl
index ce92b6d..42d14ab 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/hardware/input/VirtualStylusMotionEvent.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 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.
@@ -13,10 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.companion.virtual.camera;
-/**
- * The configuration of a single virtual camera stream.
- * @hide
- */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+package android.hardware.input;
+
+parcelable VirtualStylusMotionEvent;
diff --git a/core/java/android/hardware/input/VirtualStylusMotionEvent.java b/core/java/android/hardware/input/VirtualStylusMotionEvent.java
new file mode 100644
index 0000000..2ab76ae
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualStylusMotionEvent.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright 2023 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.hardware.input;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event describing a stylus interaction originating from a remote device.
+ *
+ * The tool type, location and action are required; tilts and pressure are optional.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+@SystemApi
+public final class VirtualStylusMotionEvent implements Parcelable {
+ private static final int TILT_MIN = -90;
+ private static final int TILT_MAX = 90;
+ private static final int PRESSURE_MIN = 0;
+ private static final int PRESSURE_MAX = 255;
+
+ /** @hide */
+ public static final int TOOL_TYPE_UNKNOWN = MotionEvent.TOOL_TYPE_UNKNOWN;
+ /** Tool type indicating that a stylus is the origin of the event. */
+ public static final int TOOL_TYPE_STYLUS = MotionEvent.TOOL_TYPE_STYLUS;
+ /** Tool type indicating that an eraser is the origin of the event. */
+ public static final int TOOL_TYPE_ERASER = MotionEvent.TOOL_TYPE_ERASER;
+ /** @hide */
+ @IntDef(prefix = { "TOOL_TYPE_" }, value = {
+ TOOL_TYPE_UNKNOWN,
+ TOOL_TYPE_STYLUS,
+ TOOL_TYPE_ERASER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ToolType {}
+
+ /** @hide */
+ public static final int ACTION_UNKNOWN = -1;
+ /**
+ * Action indicating the stylus has been pressed down to the screen. ACTION_DOWN with pressure
+ * {@code 0} indicates that the stylus is hovering over the screen, and non-zero pressure
+ * indicates that the stylus is touching the screen.
+ */
+ public static final int ACTION_DOWN = MotionEvent.ACTION_DOWN;
+ /** Action indicating the stylus has been lifted from the screen. */
+ public static final int ACTION_UP = MotionEvent.ACTION_UP;
+ /** Action indicating the stylus has been moved along the screen. */
+ public static final int ACTION_MOVE = MotionEvent.ACTION_MOVE;
+ /** @hide */
+ @IntDef(prefix = { "ACTION_" }, value = {
+ ACTION_UNKNOWN,
+ ACTION_DOWN,
+ ACTION_UP,
+ ACTION_MOVE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Action {}
+
+ @ToolType
+ private final int mToolType;
+ @Action
+ private final int mAction;
+ private final int mX;
+ private final int mY;
+ private final int mPressure;
+ private final int mTiltX;
+ private final int mTiltY;
+ private final long mEventTimeNanos;
+
+ private VirtualStylusMotionEvent(@ToolType int toolType, @Action int action, int x, int y,
+ int pressure, int tiltX, int tiltY, long eventTimeNanos) {
+ mToolType = toolType;
+ mAction = action;
+ mX = x;
+ mY = y;
+ mPressure = pressure;
+ mTiltX = tiltX;
+ mTiltY = tiltY;
+ mEventTimeNanos = eventTimeNanos;
+ }
+
+ private VirtualStylusMotionEvent(@NonNull Parcel parcel) {
+ mToolType = parcel.readInt();
+ mAction = parcel.readInt();
+ mX = parcel.readInt();
+ mY = parcel.readInt();
+ mPressure = parcel.readInt();
+ mTiltX = parcel.readInt();
+ mTiltY = parcel.readInt();
+ mEventTimeNanos = parcel.readLong();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mToolType);
+ dest.writeInt(mAction);
+ dest.writeInt(mX);
+ dest.writeInt(mY);
+ dest.writeInt(mPressure);
+ dest.writeInt(mTiltX);
+ dest.writeInt(mTiltY);
+ dest.writeLong(mEventTimeNanos);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Returns the tool type associated with this event.
+ */
+ @ToolType
+ public int getToolType() {
+ return mToolType;
+ }
+
+ /**
+ * Returns the action associated with this event.
+ */
+ @Action
+ public int getAction() {
+ return mAction;
+ }
+
+ /**
+ * Returns the x-axis location associated with this event.
+ */
+ public int getX() {
+ return mX;
+ }
+
+ /**
+ * Returns the y-axis location associated with this event.
+ */
+ public int getY() {
+ return mY;
+ }
+
+ /**
+ * Returns the pressure associated with this event. {@code 0} pressure indicates that the stylus
+ * is hovering, otherwise the stylus is touching the screen. Returns {@code 255} if omitted.
+ */
+ public int getPressure() {
+ return mPressure;
+ }
+
+ /**
+ * Returns the plane angle (in degrees, in the range of [{@code -90}, {@code 90}]) between the
+ * y-z plane and the plane containing both the stylus axis and the y axis. A positive tiltX is
+ * to the right, in the direction of increasing x values. {@code 0} tilt indicates that the
+ * stylus is perpendicular to the x-axis. Returns {@code 0} if omitted.
+ *
+ * @see Builder#setTiltX
+ */
+ public int getTiltX() {
+ return mTiltX;
+ }
+
+ /**
+ * Returns the plane angle (in degrees, in the range of [{@code -90}, {@code 90}]) between the
+ * x-z plane and the plane containing both the stylus axis and the x axis. A positive tiltY is
+ * towards the user, in the direction of increasing y values. {@code 0} tilt indicates that the
+ * stylus is perpendicular to the y-axis. Returns {@code 0} if omitted.
+ *
+ * @see Builder#setTiltY
+ */
+ public int getTiltY() {
+ return mTiltY;
+ }
+
+ /**
+ * Returns the time this event occurred, in the {@link SystemClock#uptimeMillis()} time base but
+ * with nanosecond (instead of millisecond) precision.
+ *
+ * @see InputEvent#getEventTime()
+ */
+ public long getEventTimeNanos() {
+ return mEventTimeNanos;
+ }
+
+ /**
+ * Builder for {@link VirtualStylusMotionEvent}.
+ */
+ @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
+ public static final class Builder {
+
+ @ToolType
+ private int mToolType = TOOL_TYPE_UNKNOWN;
+ @Action
+ private int mAction = ACTION_UNKNOWN;
+ private int mX = 0;
+ private int mY = 0;
+ private boolean mIsXSet = false;
+ private boolean mIsYSet = false;
+ private int mPressure = PRESSURE_MAX;
+ private int mTiltX = 0;
+ private int mTiltY = 0;
+ private long mEventTimeNanos = 0L;
+
+ /**
+ * Creates a {@link VirtualStylusMotionEvent} object with the current builder configuration.
+ *
+ * @throws IllegalArgumentException if one of the required arguments (action, tool type,
+ * x-axis location and y-axis location) is missing.
+ * {@link VirtualStylusMotionEvent} for a detailed explanation.
+ */
+ @NonNull
+ public VirtualStylusMotionEvent build() {
+ if (mToolType == TOOL_TYPE_UNKNOWN) {
+ throw new IllegalArgumentException(
+ "Cannot build stylus motion event with unset tool type");
+ }
+ if (mAction == ACTION_UNKNOWN) {
+ throw new IllegalArgumentException(
+ "Cannot build stylus motion event with unset action");
+ }
+ if (!mIsXSet) {
+ throw new IllegalArgumentException(
+ "Cannot build stylus motion event with unset x-axis location");
+ }
+ if (!mIsYSet) {
+ throw new IllegalArgumentException(
+ "Cannot build stylus motion event with unset y-axis location");
+ }
+ return new VirtualStylusMotionEvent(mToolType, mAction, mX, mY, mPressure, mTiltX,
+ mTiltY, mEventTimeNanos);
+ }
+
+ /**
+ * Sets the tool type of the event.
+ *
+ * @return this builder, to allow for chaining of calls
+ */
+ @NonNull
+ public Builder setToolType(@ToolType int toolType) {
+ if (toolType != TOOL_TYPE_STYLUS && toolType != TOOL_TYPE_ERASER) {
+ throw new IllegalArgumentException("Unsupported stylus tool type: " + toolType);
+ }
+ mToolType = toolType;
+ return this;
+ }
+
+ /**
+ * Sets the action of the event.
+ *
+ * @return this builder, to allow for chaining of calls
+ */
+ @NonNull
+ public Builder setAction(@Action int action) {
+ if (action != ACTION_DOWN && action != ACTION_UP && action != ACTION_MOVE) {
+ throw new IllegalArgumentException("Unsupported stylus action : " + action);
+ }
+ mAction = action;
+ return this;
+ }
+
+ /**
+ * Sets the x-axis location of the event.
+ *
+ * @return this builder, to allow for chaining of calls
+ */
+ @NonNull
+ public Builder setX(int absX) {
+ mX = absX;
+ mIsXSet = true;
+ return this;
+ }
+
+ /**
+ * Sets the y-axis location of the event.
+ *
+ * @return this builder, to allow for chaining of calls
+ */
+ @NonNull
+ public Builder setY(int absY) {
+ mY = absY;
+ mIsYSet = true;
+ return this;
+ }
+
+ /**
+ * Sets the pressure of the event. {@code 0} pressure indicates that the stylus is hovering,
+ * otherwise the stylus is touching the screen. This field is optional and can be omitted
+ * (defaults to {@code 255}).
+ *
+ * @param pressure The pressure of the stylus.
+ *
+ * @throws IllegalArgumentException if the pressure is smaller than 0 or greater than 255.
+ *
+ * @return this builder, to allow for chaining of calls
+ */
+ @NonNull
+ public Builder setPressure(
+ @IntRange(from = PRESSURE_MIN, to = PRESSURE_MAX) int pressure) {
+ if (pressure < PRESSURE_MIN || pressure > PRESSURE_MAX) {
+ throw new IllegalArgumentException(
+ "Pressure should be between " + PRESSURE_MIN + " and " + PRESSURE_MAX);
+ }
+ mPressure = pressure;
+ return this;
+ }
+
+ /**
+ * Sets the x-axis tilt of the event in degrees. {@code 0} tilt indicates that the stylus is
+ * perpendicular to the x-axis. This field is optional and can be omitted (defaults to
+ * {@code 0}). Both x-axis tilt and y-axis tilt are used to derive the tilt and orientation
+ * of the stylus, given by {@link MotionEvent#AXIS_TILT} and
+ * {@link MotionEvent#AXIS_ORIENTATION} respectively.
+ *
+ * @throws IllegalArgumentException if the tilt is smaller than -90 or greater than 90.
+ *
+ * @return this builder, to allow for chaining of calls
+ *
+ * @see VirtualStylusMotionEvent#getTiltX
+ * @see <a href="https://source.android.com/docs/core/interaction/input/touch-devices#orientation-and-tilt-fields">
+ * Stylus tilt and orientation</a>
+ */
+ @NonNull
+ public Builder setTiltX(@IntRange(from = TILT_MIN, to = TILT_MAX) int tiltX) {
+ validateTilt(tiltX);
+ mTiltX = tiltX;
+ return this;
+ }
+
+ /**
+ * Sets the y-axis tilt of the event in degrees. {@code 0} tilt indicates that the stylus is
+ * perpendicular to the y-axis. This field is optional and can be omitted (defaults to
+ * {@code 0}). Both x-axis tilt and y-axis tilt are used to derive the tilt and orientation
+ * of the stylus, given by {@link MotionEvent#AXIS_TILT} and
+ * {@link MotionEvent#AXIS_ORIENTATION} respectively.
+ *
+ * @throws IllegalArgumentException if the tilt is smaller than -90 or greater than 90.
+ *
+ * @return this builder, to allow for chaining of calls
+ *
+ * @see VirtualStylusMotionEvent#getTiltY
+ * @see <a href="https://source.android.com/docs/core/interaction/input/touch-devices#orientation-and-tilt-fields">
+ * Stylus tilt and orientation</a>
+ */
+ @NonNull
+ public Builder setTiltY(@IntRange(from = TILT_MIN, to = TILT_MAX) int tiltY) {
+ validateTilt(tiltY);
+ mTiltY = tiltY;
+ return this;
+ }
+
+ /**
+ * Sets the time (in nanoseconds) when this specific event was generated. This may be
+ * obtained from {@link SystemClock#uptimeMillis()} (with nanosecond precision instead of
+ * millisecond), but can be different depending on the use case.
+ * This field is optional and can be omitted.
+ *
+ * @return this builder, to allow for chaining of calls
+ * @see InputEvent#getEventTime()
+ */
+ @NonNull
+ public Builder setEventTimeNanos(long eventTimeNanos) {
+ if (eventTimeNanos < 0L) {
+ throw new IllegalArgumentException("Event time cannot be negative");
+ }
+ mEventTimeNanos = eventTimeNanos;
+ return this;
+ }
+
+ private void validateTilt(int tilt) {
+ if (tilt < TILT_MIN || tilt > TILT_MAX) {
+ throw new IllegalArgumentException(
+ "Tilt must be between " + TILT_MIN + " and " + TILT_MAX);
+ }
+ }
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<VirtualStylusMotionEvent> CREATOR =
+ new Parcelable.Creator<>() {
+ public VirtualStylusMotionEvent createFromParcel(Parcel source) {
+ return new VirtualStylusMotionEvent(source);
+ }
+ public VirtualStylusMotionEvent[] newArray(int size) {
+ return new VirtualStylusMotionEvent[size];
+ }
+ };
+}
diff --git a/core/java/android/hardware/input/VirtualTouchDeviceConfig.java b/core/java/android/hardware/input/VirtualTouchDeviceConfig.java
new file mode 100644
index 0000000..2e2cfab
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualTouchDeviceConfig.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023 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.hardware.input;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.os.Parcel;
+
+/**
+ * Configurations to create a virtual touch-based device.
+ *
+ * @hide
+ */
+abstract class VirtualTouchDeviceConfig extends VirtualInputDeviceConfig {
+
+ /** The touch device width. */
+ private final int mWidth;
+ /** The touch device height. */
+ private final int mHeight;
+
+ VirtualTouchDeviceConfig(@NonNull Builder<? extends Builder<?>> builder) {
+ super(builder);
+ mWidth = builder.mWidth;
+ mHeight = builder.mHeight;
+ }
+
+ VirtualTouchDeviceConfig(@NonNull Parcel in) {
+ super(in);
+ mWidth = in.readInt();
+ mHeight = in.readInt();
+ }
+
+ /** Returns the touch device width. */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /** Returns the touch device height. */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mWidth);
+ dest.writeInt(mHeight);
+ }
+
+ @Override
+ @NonNull
+ String additionalFieldsToString() {
+ return " width=" + mWidth + " height=" + mHeight;
+ }
+
+ /**
+ * Builder for creating a {@link VirtualTouchDeviceConfig}.
+ *
+ * @param <T> The subclass to be built.
+ */
+ abstract static class Builder<T extends Builder<T>>
+ extends VirtualInputDeviceConfig.Builder<T> {
+
+ private final int mWidth;
+ private final int mHeight;
+
+ /**
+ * Creates a new instance for the given dimensions of the touch device.
+ *
+ * <p>The dimensions are not pixels but in the screen's raw coordinate space. They do
+ * not necessarily have to correspond to the display size or aspect ratio. In this case the
+ * framework will handle the scaling appropriately.
+ *
+ * @param touchDeviceWidth The width of the touch device.
+ * @param touchDeviceHeight The height of the touch device.
+ */
+ Builder(@IntRange(from = 1) int touchDeviceWidth,
+ @IntRange(from = 1) int touchDeviceHeight) {
+ if (touchDeviceHeight <= 0 || touchDeviceWidth <= 0) {
+ throw new IllegalArgumentException(
+ "Cannot create a virtual touch-based device, dimensions must be "
+ + "positive. Got: (" + touchDeviceHeight + ", "
+ + touchDeviceWidth + ")");
+ }
+ mHeight = touchDeviceHeight;
+ mWidth = touchDeviceWidth;
+ }
+ }
+}
diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
index 6308459..851cee6 100644
--- a/core/java/android/hardware/input/VirtualTouchscreenConfig.java
+++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
@@ -23,38 +23,19 @@
import android.os.Parcelable;
/**
- * Configurations to create virtual touchscreen.
+ * Configurations to create a virtual touchscreen.
*
* @hide
*/
@SystemApi
-public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig implements Parcelable {
-
- /** The touchscreen width. */
- private final int mWidth;
- /** The touchscreen height. */
- private final int mHeight;
+public final class VirtualTouchscreenConfig extends VirtualTouchDeviceConfig implements Parcelable {
private VirtualTouchscreenConfig(@NonNull Builder builder) {
super(builder);
- mWidth = builder.mWidth;
- mHeight = builder.mHeight;
}
private VirtualTouchscreenConfig(@NonNull Parcel in) {
super(in);
- mWidth = in.readInt();
- mHeight = in.readInt();
- }
-
- /** Returns the touchscreen width. */
- public int getWidth() {
- return mWidth;
- }
-
- /** Returns the touchscreen height. */
- public int getHeight() {
- return mHeight;
}
@Override
@@ -65,19 +46,11 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
- dest.writeInt(mWidth);
- dest.writeInt(mHeight);
- }
-
- @Override
- @NonNull
- String additionalFieldsToString() {
- return " width=" + mWidth + " height=" + mHeight;
}
@NonNull
public static final Creator<VirtualTouchscreenConfig> CREATOR =
- new Creator<VirtualTouchscreenConfig>() {
+ new Creator<>() {
@Override
public VirtualTouchscreenConfig createFromParcel(Parcel in) {
return new VirtualTouchscreenConfig(in);
@@ -92,9 +65,7 @@
/**
* Builder for creating a {@link VirtualTouchscreenConfig}.
*/
- public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
- private int mWidth;
- private int mHeight;
+ public static final class Builder extends VirtualTouchDeviceConfig.Builder<Builder> {
/**
* Creates a new instance for the given dimensions of the {@link VirtualTouchscreen}.
@@ -108,14 +79,7 @@
*/
public Builder(@IntRange(from = 1) int touchscreenWidth,
@IntRange(from = 1) int touchscreenHeight) {
- if (touchscreenHeight <= 0 || touchscreenWidth <= 0) {
- throw new IllegalArgumentException(
- "Cannot create a virtual touchscreen, touchscreen dimensions must be "
- + "positive. Got: (" + touchscreenHeight + ", "
- + touchscreenWidth + ")");
- }
- mHeight = touchscreenHeight;
- mWidth = touchscreenWidth;
+ super(touchscreenWidth, touchscreenHeight);
}
/**
diff --git a/core/java/android/os/BugreportParams.java b/core/java/android/os/BugreportParams.java
index 8510084..f2ef185 100644
--- a/core/java/android/os/BugreportParams.java
+++ b/core/java/android/os/BugreportParams.java
@@ -21,6 +21,7 @@
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.app.admin.flags.Flags;
+import android.compat.annotation.UnsupportedAppUsage;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -131,6 +132,7 @@
*/
@TestApi
@FlaggedApi(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED)
+ @UnsupportedAppUsage
public static final int BUGREPORT_MODE_ONBOARDING = IDumpstate.BUGREPORT_MODE_ONBOARDING;
/**
diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java
index 746278f..e6bfcd7 100644
--- a/core/java/android/os/PerformanceHintManager.java
+++ b/core/java/android/os/PerformanceHintManager.java
@@ -183,13 +183,14 @@
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = {"CPU_LOAD_"}, value = {
+ @IntDef(prefix = {"CPU_LOAD_", "GPU_LOAD_"}, value = {
CPU_LOAD_UP,
CPU_LOAD_DOWN,
CPU_LOAD_RESET,
CPU_LOAD_RESUME,
GPU_LOAD_UP,
- GPU_LOAD_DOWN
+ GPU_LOAD_DOWN,
+ GPU_LOAD_RESET
})
public @interface Hint {}
diff --git a/core/java/android/os/storage/StorageManagerInternal.java b/core/java/android/os/storage/StorageManagerInternal.java
index 8961846..6995ea8 100644
--- a/core/java/android/os/storage/StorageManagerInternal.java
+++ b/core/java/android/os/storage/StorageManagerInternal.java
@@ -193,7 +193,7 @@
* @see com.android.server.pm.Installer#createFsveritySetupAuthToken()
*/
public abstract IInstalld.IFsveritySetupAuthToken createFsveritySetupAuthToken(
- ParcelFileDescriptor authFd, int appUid, @UserIdInt int userId) throws IOException;
+ ParcelFileDescriptor authFd, int uid) throws IOException;
/**
* A proxy call to the corresponding method in Installer.
diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl
index 7cecfdc..471f95b 100644
--- a/core/java/android/permission/IPermissionManager.aidl
+++ b/core/java/android/permission/IPermissionManager.aidl
@@ -92,7 +92,7 @@
boolean isAutoRevokeExempted(String packageName, int userId);
- void registerAttributionSource(in AttributionSourceState source);
+ IBinder registerAttributionSource(in AttributionSourceState source);
boolean isRegisteredAttributionSource(in AttributionSourceState source);
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index 91adc37..4af6e3a 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -23,6 +23,7 @@
import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED;
import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET;
import static android.os.Build.VERSION_CODES.S;
+import static android.permission.flags.Flags.serverSideAttributionRegistration;
import android.Manifest;
import android.annotation.CheckResult;
@@ -59,6 +60,7 @@
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
+import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
@@ -1464,13 +1466,19 @@
// We use a shared static token for sources that are not registered since the token's
// only used for process death detection. If we are about to use the source for security
// enforcement we need to replace the binder with a unique one.
- final AttributionSource registeredSource = source.withToken(new Binder());
try {
- mPermissionManager.registerAttributionSource(registeredSource.asState());
+ if (serverSideAttributionRegistration()) {
+ IBinder newToken = mPermissionManager.registerAttributionSource(source.asState());
+ return source.withToken(newToken);
+ } else {
+ AttributionSource registeredSource = source.withToken(new Binder());
+ mPermissionManager.registerAttributionSource(registeredSource.asState());
+ return registeredSource;
+ }
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
- return registeredSource;
+ return source;
}
/**
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 60143cc..39b6aeb 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -45,7 +45,8 @@
}
flag {
- name: "enhanced_confirmation_mode_apis"
+ name: "enhanced_confirmation_mode_apis_enabled"
+ is_fixed_read_only: true
namespace: "permissions"
description: "enable enhanced confirmation mode apis"
bug: "310220212"
@@ -73,6 +74,13 @@
}
flag {
+ name: "server_side_attribution_registration"
+ namespace: "permissions"
+ description: "controls whether the binder representing an AttributionSource is created in the system server, or client process"
+ bug: "310953959"
+}
+
+flag {
name: "wallet_role_enabled"
namespace: "wallet_integration"
description: "This flag is used to enabled the Wallet Role for all users on the device"
diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java
index 03ebae5..90049e6 100644
--- a/core/java/android/service/notification/ZenDeviceEffects.java
+++ b/core/java/android/service/notification/ZenDeviceEffects.java
@@ -20,7 +20,6 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.TestApi;
import android.app.Flags;
import android.os.Parcel;
import android.os.Parcelable;
@@ -37,8 +36,8 @@
@FlaggedApi(Flags.FLAG_MODES_API)
public final class ZenDeviceEffects implements Parcelable {
- /** Used to track which rule variables have been modified by the user.
- * Should be checked against the bitmask {@link #getUserModifiedFields()}.
+ /**
+ * Enum for the user-modifiable fields in this object.
* @hide
*/
@IntDef(flag = true, prefix = { "FIELD_" }, value = {
@@ -59,52 +58,42 @@
/**
* @hide
*/
- @TestApi
public static final int FIELD_GRAYSCALE = 1 << 0;
/**
* @hide
*/
- @TestApi
public static final int FIELD_SUPPRESS_AMBIENT_DISPLAY = 1 << 1;
/**
* @hide
*/
- @TestApi
public static final int FIELD_DIM_WALLPAPER = 1 << 2;
/**
* @hide
*/
- @TestApi
public static final int FIELD_NIGHT_MODE = 1 << 3;
/**
* @hide
*/
- @TestApi
public static final int FIELD_DISABLE_AUTO_BRIGHTNESS = 1 << 4;
/**
* @hide
*/
- @TestApi
public static final int FIELD_DISABLE_TAP_TO_WAKE = 1 << 5;
/**
* @hide
*/
- @TestApi
public static final int FIELD_DISABLE_TILT_TO_WAKE = 1 << 6;
/**
* @hide
*/
- @TestApi
public static final int FIELD_DISABLE_TOUCH = 1 << 7;
/**
* @hide
*/
- @TestApi
public static final int FIELD_MINIMIZE_RADIO_USAGE = 1 << 8;
/**
* @hide
*/
- @TestApi
public static final int FIELD_MAXIMIZE_DOZE = 1 << 9;
private final boolean mGrayscale;
@@ -119,13 +108,10 @@
private final boolean mMinimizeRadioUsage;
private final boolean mMaximizeDoze;
- private final @ModifiableField int mUserModifiedFields; // Bitwise representation
-
private ZenDeviceEffects(boolean grayscale, boolean suppressAmbientDisplay,
boolean dimWallpaper, boolean nightMode, boolean disableAutoBrightness,
boolean disableTapToWake, boolean disableTiltToWake, boolean disableTouch,
- boolean minimizeRadioUsage, boolean maximizeDoze,
- @ModifiableField int userModifiedFields) {
+ boolean minimizeRadioUsage, boolean maximizeDoze) {
mGrayscale = grayscale;
mSuppressAmbientDisplay = suppressAmbientDisplay;
mDimWallpaper = dimWallpaper;
@@ -136,7 +122,6 @@
mDisableTouch = disableTouch;
mMinimizeRadioUsage = minimizeRadioUsage;
mMaximizeDoze = maximizeDoze;
- mUserModifiedFields = userModifiedFields;
}
@Override
@@ -153,15 +138,14 @@
&& this.mDisableTiltToWake == that.mDisableTiltToWake
&& this.mDisableTouch == that.mDisableTouch
&& this.mMinimizeRadioUsage == that.mMinimizeRadioUsage
- && this.mMaximizeDoze == that.mMaximizeDoze
- && this.mUserModifiedFields == that.mUserModifiedFields;
+ && this.mMaximizeDoze == that.mMaximizeDoze;
}
@Override
public int hashCode() {
return Objects.hash(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper, mNightMode,
mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake, mDisableTouch,
- mMinimizeRadioUsage, mMaximizeDoze, mUserModifiedFields);
+ mMinimizeRadioUsage, mMaximizeDoze);
}
@Override
@@ -177,11 +161,11 @@
if (mDisableTouch) effects.add("disableTouch");
if (mMinimizeRadioUsage) effects.add("minimizeRadioUsage");
if (mMaximizeDoze) effects.add("maximizeDoze");
- return "[" + String.join(", ", effects) + "]"
- + " userModifiedFields: " + modifiedFieldsToString(mUserModifiedFields);
+ return "[" + String.join(", ", effects) + "]";
}
- private String modifiedFieldsToString(int bitmask) {
+ /** @hide */
+ public static String fieldsToString(@ModifiableField int bitmask) {
ArrayList<String> modified = new ArrayList<>();
if ((bitmask & FIELD_GRAYSCALE) != 0) {
modified.add("FIELD_GRAYSCALE");
@@ -312,7 +296,7 @@
return new ZenDeviceEffects(in.readBoolean(),
in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(),
in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(),
- in.readBoolean(), in.readInt());
+ in.readBoolean());
}
@Override
@@ -321,16 +305,6 @@
}
};
- /**
- * Gets the bitmask representing which fields are user modified. Bits are set using
- * {@link ModifiableField}.
- * @hide
- */
- @TestApi
- public @ModifiableField int getUserModifiedFields() {
- return mUserModifiedFields;
- }
-
@Override
public int describeContents() {
return 0;
@@ -348,7 +322,6 @@
dest.writeBoolean(mDisableTouch);
dest.writeBoolean(mMinimizeRadioUsage);
dest.writeBoolean(mMaximizeDoze);
- dest.writeInt(mUserModifiedFields);
}
/** Builder class for {@link ZenDeviceEffects} objects. */
@@ -365,7 +338,6 @@
private boolean mDisableTouch;
private boolean mMinimizeRadioUsage;
private boolean mMaximizeDoze;
- private @ModifiableField int mUserModifiedFields;
/**
* Instantiates a new {@link ZenPolicy.Builder} with all effects set to default (disabled).
@@ -388,7 +360,6 @@
mDisableTouch = zenDeviceEffects.shouldDisableTouch();
mMinimizeRadioUsage = zenDeviceEffects.shouldMinimizeRadioUsage();
mMaximizeDoze = zenDeviceEffects.shouldMaximizeDoze();
- mUserModifiedFields = zenDeviceEffects.mUserModifiedFields;
}
/**
@@ -510,24 +481,13 @@
return this;
}
- /**
- * Sets the bitmask representing which fields are user modified. See the FIELD_ constants.
- * @hide
- */
- @TestApi
- @NonNull
- public Builder setUserModifiedFields(@ModifiableField int userModifiedFields) {
- mUserModifiedFields = userModifiedFields;
- return this;
- }
-
/** Builds a {@link ZenDeviceEffects} object based on the builder's state. */
@NonNull
public ZenDeviceEffects build() {
return new ZenDeviceEffects(mGrayscale,
mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness,
mDisableTapToWake, mDisableTiltToWake, mDisableTouch, mMinimizeRadioUsage,
- mMaximizeDoze, mUserModifiedFields);
+ mMaximizeDoze);
}
}
}
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 54248be..c479877 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -205,8 +205,8 @@
private static final String ALLOW_ATT_SCREEN_ON = "visualScreenOn";
private static final String ALLOW_ATT_CONV = "convos";
private static final String ALLOW_ATT_CONV_FROM = "convosFrom";
- private static final String ALLOW_ATT_CHANNELS = "channels";
- private static final String USER_MODIFIED_FIELDS = "policyUserModifiedFields";
+ private static final String ALLOW_ATT_CHANNELS = "priorityChannels";
+ private static final String POLICY_USER_MODIFIED_FIELDS = "policyUserModifiedFields";
private static final String DISALLOW_TAG = "disallow";
private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects";
private static final String STATE_TAG = "state";
@@ -806,6 +806,9 @@
rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC);
rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN);
rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0);
+ rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0);
+ rt.zenDeviceEffectsUserModifiedFields = safeInt(parser,
+ DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0);
Long deletionInstant = tryParseLong(
parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null);
if (deletionInstant != null) {
@@ -858,6 +861,9 @@
}
out.attributeInt(null, RULE_ATT_TYPE, rule.type);
out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields);
+ out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields);
+ out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS,
+ rule.zenDeviceEffectsUserModifiedFields);
if (rule.deletionInstant != null) {
out.attributeLong(null, RULE_ATT_DELETION_INSTANT,
rule.deletionInstant.toEpochMilli());
@@ -919,12 +925,11 @@
final int events = safeInt(parser, ALLOW_ATT_EVENTS, ZenPolicy.STATE_UNSET);
final int reminders = safeInt(parser, ALLOW_ATT_REMINDERS, ZenPolicy.STATE_UNSET);
if (Flags.modesApi()) {
- final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.CHANNEL_TYPE_UNSET);
- if (channels != ZenPolicy.CHANNEL_TYPE_UNSET) {
- builder.allowChannels(channels);
+ final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.STATE_UNSET);
+ if (channels != ZenPolicy.STATE_UNSET) {
+ builder.allowPriorityChannels(channels == ZenPolicy.STATE_ALLOW);
policySet = true;
}
- builder.setUserModifiedFields(safeInt(parser, USER_MODIFIED_FIELDS, 0));
}
if (calls != ZenPolicy.PEOPLE_TYPE_UNSET) {
@@ -1036,8 +1041,7 @@
out);
if (Flags.modesApi()) {
- writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getAllowedChannels(), out);
- out.attributeInt(null, USER_MODIFIED_FIELDS, policy.getUserModifiedFields());
+ writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannels(), out);
}
}
@@ -1053,7 +1057,7 @@
out.attributeInt(null, attr, val);
}
} else if (Flags.modesApi() && Objects.equals(attr, ALLOW_ATT_CHANNELS)) {
- if (val != ZenPolicy.CHANNEL_TYPE_UNSET) {
+ if (val != ZenPolicy.STATE_UNSET) {
out.attributeInt(null, attr, val);
}
} else {
@@ -1083,7 +1087,6 @@
.setShouldMinimizeRadioUsage(
safeBoolean(parser, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE, false))
.setShouldMaximizeDoze(safeBoolean(parser, DEVICE_EFFECT_MAXIMIZE_DOZE, false))
- .setUserModifiedFields(safeInt(parser, DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0))
.build();
return deviceEffects.hasEffects() ? deviceEffects : null;
@@ -1108,8 +1111,6 @@
writeBooleanIfTrue(out, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE,
deviceEffects.shouldMinimizeRadioUsage());
writeBooleanIfTrue(out, DEVICE_EFFECT_MAXIMIZE_DOZE, deviceEffects.shouldMaximizeDoze());
- out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS,
- deviceEffects.getUserModifiedFields());
}
private static void writeBooleanIfTrue(TypedXmlSerializer out, String att, boolean value)
@@ -1238,8 +1239,7 @@
}
if (Flags.modesApi()) {
- builder.allowChannels(allowPriorityChannels ? ZenPolicy.CHANNEL_TYPE_PRIORITY
- : ZenPolicy.CHANNEL_TYPE_NONE);
+ builder.allowPriorityChannels(allowPriorityChannels);
}
return builder.build();
}
@@ -1369,7 +1369,7 @@
int state = defaultPolicy.state;
if (Flags.modesApi()) {
state = Policy.policyState(defaultPolicy.hasPriorityChannels(),
- getAllowPriorityChannelsWithDefault(zenPolicy.getAllowedChannels(),
+ ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannels(),
DEFAULT_ALLOW_PRIORITY_CHANNELS));
}
@@ -1412,24 +1412,6 @@
}
/**
- * Gets whether priority channels are permitted by this channel type, with the specified
- * default if the value is unset. This effectively converts the channel enum to a boolean,
- * where "true" indicates priority channels are allowed to break through and "false" means
- * they are not.
- */
- public static boolean getAllowPriorityChannelsWithDefault(
- @ZenPolicy.ChannelType int channelType, boolean defaultAllowChannels) {
- switch (channelType) {
- case ZenPolicy.CHANNEL_TYPE_PRIORITY:
- return true;
- case ZenPolicy.CHANNEL_TYPE_NONE:
- return false;
- default:
- return defaultAllowChannels;
- }
- }
-
- /**
* Maps NotificationManager.Policy senders type to ZenPolicy.PeopleType
*/
public static @ZenPolicy.PeopleType int getZenPolicySenders(int senders) {
@@ -2060,7 +2042,9 @@
public String triggerDescription;
public String iconResName;
public boolean allowManualInvocation;
- public int userModifiedFields;
+ @AutomaticZenRule.ModifiableField public int userModifiedFields;
+ @ZenPolicy.ModifiableField public int zenPolicyUserModifiedFields;
+ @ZenDeviceEffects.ModifiableField public int zenDeviceEffectsUserModifiedFields;
@Nullable public Instant deletionInstant; // Only set on deleted rules.
public ZenRule() { }
@@ -2095,6 +2079,8 @@
triggerDescription = source.readString();
type = source.readInt();
userModifiedFields = source.readInt();
+ zenPolicyUserModifiedFields = source.readInt();
+ zenDeviceEffectsUserModifiedFields = source.readInt();
if (source.readInt() == 1) {
deletionInstant = Instant.ofEpochMilli(source.readLong());
}
@@ -2102,15 +2088,21 @@
}
/**
- * @see AutomaticZenRule#canUpdate()
+ * Whether this ZenRule can be updated by an app. In general, rules that have been
+ * customized by the user cannot be further updated by an app, with some exceptions:
+ * <ul>
+ * <li>Non user-configurable fields, like type, icon, configurationActivity, etc.
+ * <li>Name, if the name was not specifically modified by the user (to support language
+ * switches).
+ * </ul>
*/
@FlaggedApi(Flags.FLAG_MODES_API)
public boolean canBeUpdatedByApp() {
// The rule is considered updateable if its bitmask has no user modifications, and
// the bitmasks of the policy and device effects have no modification.
return userModifiedFields == 0
- && (zenPolicy == null || zenPolicy.getUserModifiedFields() == 0)
- && (zenDeviceEffects == null || zenDeviceEffects.getUserModifiedFields() == 0);
+ && zenPolicyUserModifiedFields == 0
+ && zenDeviceEffectsUserModifiedFields == 0;
}
@Override
@@ -2158,6 +2150,8 @@
dest.writeString(triggerDescription);
dest.writeInt(type);
dest.writeInt(userModifiedFields);
+ dest.writeInt(zenPolicyUserModifiedFields);
+ dest.writeInt(zenDeviceEffectsUserModifiedFields);
if (deletionInstant != null) {
dest.writeInt(1);
dest.writeLong(deletionInstant.toEpochMilli());
@@ -2192,8 +2186,20 @@
.append(",allowManualInvocation=").append(allowManualInvocation)
.append(",iconResName=").append(iconResName)
.append(",triggerDescription=").append(triggerDescription)
- .append(",type=").append(type)
- .append(",userModifiedFields=").append(userModifiedFields);
+ .append(",type=").append(type);
+ if (userModifiedFields != 0) {
+ sb.append(",userModifiedFields=")
+ .append(AutomaticZenRule.fieldsToString(userModifiedFields));
+ }
+ if (zenPolicyUserModifiedFields != 0) {
+ sb.append(",zenPolicyUserModifiedFields=")
+ .append(ZenPolicy.fieldsToString(zenPolicyUserModifiedFields));
+ }
+ if (zenDeviceEffectsUserModifiedFields != 0) {
+ sb.append(",zenDeviceEffectsUserModifiedFields=")
+ .append(ZenDeviceEffects.fieldsToString(
+ zenDeviceEffectsUserModifiedFields));
+ }
if (deletionInstant != null) {
sb.append(",deletionInstant=").append(deletionInstant);
}
@@ -2257,6 +2263,9 @@
&& Objects.equals(other.triggerDescription, triggerDescription)
&& other.type == type
&& other.userModifiedFields == userModifiedFields
+ && other.zenPolicyUserModifiedFields == zenPolicyUserModifiedFields
+ && other.zenDeviceEffectsUserModifiedFields
+ == zenDeviceEffectsUserModifiedFields
&& Objects.equals(other.deletionInstant, deletionInstant);
}
@@ -2269,7 +2278,8 @@
return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
component, configurationActivity, pkg, id, enabler, zenPolicy,
zenDeviceEffects, modified, allowManualInvocation, iconResName,
- triggerDescription, type, userModifiedFields, deletionInstant);
+ triggerDescription, type, userModifiedFields, zenPolicyUserModifiedFields,
+ zenDeviceEffectsUserModifiedFields, deletionInstant);
}
return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index 8477eb7..fb491d0 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -45,8 +45,8 @@
*/
public final class ZenPolicy implements Parcelable {
- /** Used to track which rule variables have been modified by the user.
- * Should be checked against the bitmask {@link #getUserModifiedFields()}.
+ /**
+ * Enum for the user-modifiable fields in this object.
* @hide
*/
@IntDef(flag = true, prefix = { "FIELD_" }, value = {
@@ -76,7 +76,6 @@
* the same time.
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_MESSAGES = 1 << 0;
/**
@@ -84,7 +83,6 @@
* the same time.
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_CALLS = 1 << 1;
/**
@@ -92,13 +90,11 @@
* set at the same time.
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_CONVERSATIONS = 1 << 2;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_ALLOW_CHANNELS = 1 << 3;
/**
@@ -109,73 +105,61 @@
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 1 << 5;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 6;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 1 << 7;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 1 << 8;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 1 << 9;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1 << 10;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_LIGHTS = 1 << 11;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_PEEK = 1 << 12;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 1 << 13;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_BADGE = 1 << 14;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_AMBIENT = 1 << 15;
/**
* @hide
*/
- @TestApi
@FlaggedApi(Flags.FLAG_MODES_API)
public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 1 << 16;
@@ -184,8 +168,8 @@
private @PeopleType int mPriorityMessages = PEOPLE_TYPE_UNSET;
private @PeopleType int mPriorityCalls = PEOPLE_TYPE_UNSET;
private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET;
- private @ChannelType int mAllowChannels = CHANNEL_TYPE_UNSET;
- private final @ModifiableField int mUserModifiedFields; // Bitwise representation
+ @FlaggedApi(Flags.FLAG_MODES_API)
+ private @ChannelType int mAllowChannels = CHANNEL_POLICY_UNSET;
/** @hide */
@IntDef(prefix = { "PRIORITY_CATEGORY_" }, value = {
@@ -354,56 +338,52 @@
*/
public static final int STATE_DISALLOW = 2;
- /** @hide */
- @IntDef(prefix = { "CHANNEL_TYPE_" }, value = {
- CHANNEL_TYPE_UNSET,
- CHANNEL_TYPE_PRIORITY,
- CHANNEL_TYPE_NONE,
+ @IntDef(prefix = { "CHANNEL_POLICY_" }, value = {
+ CHANNEL_POLICY_UNSET,
+ CHANNEL_POLICY_PRIORITY,
+ CHANNEL_POLICY_NONE,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface ChannelType {}
+ private @interface ChannelType {}
/**
* Indicates no explicit setting for which channels may bypass DND when this policy is active.
- * Defaults to {@link #CHANNEL_TYPE_PRIORITY}.
+ * Defaults to {@link #CHANNEL_POLICY_PRIORITY}.
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- public static final int CHANNEL_TYPE_UNSET = 0;
+ private static final int CHANNEL_POLICY_UNSET = 0;
/**
* Indicates that channels marked as {@link NotificationChannel#canBypassDnd()} can bypass DND
* when this policy is active.
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- public static final int CHANNEL_TYPE_PRIORITY = 1;
+ private static final int CHANNEL_POLICY_PRIORITY = 1;
/**
* Indicates that no channels can bypass DND when this policy is active, even those marked as
* {@link NotificationChannel#canBypassDnd()}.
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- public static final int CHANNEL_TYPE_NONE = 2;
+ private static final int CHANNEL_POLICY_NONE = 2;
/** @hide */
public ZenPolicy() {
mPriorityCategories = new ArrayList<>(Collections.nCopies(NUM_PRIORITY_CATEGORIES, 0));
mVisualEffects = new ArrayList<>(Collections.nCopies(NUM_VISUAL_EFFECTS, 0));
- mUserModifiedFields = 0;
}
/** @hide */
@FlaggedApi(Flags.FLAG_MODES_API)
public ZenPolicy(List<Integer> priorityCategories, List<Integer> visualEffects,
@PeopleType int priorityMessages, @PeopleType int priorityCalls,
- @ConversationSenders int conversationSenders, @ChannelType int allowChannels,
- @ModifiableField int userModifiedFields) {
+ @ConversationSenders int conversationSenders, @ChannelType int allowChannels) {
mPriorityCategories = priorityCategories;
mVisualEffects = visualEffects;
mPriorityMessages = priorityMessages;
mPriorityCalls = priorityCalls;
mConversationSenders = conversationSenders;
mAllowChannels = allowChannels;
- mUserModifiedFields = userModifiedFields;
}
/**
@@ -584,16 +564,21 @@
}
/**
- * Which types of {@link NotificationChannel channels} this policy allows to bypass DND. When
- * this value is {@link #CHANNEL_TYPE_PRIORITY priority} channels, any channel with
- * canBypassDnd() may bypass DND; when it is {@link #CHANNEL_TYPE_NONE none}, even channels
- * with canBypassDnd() will be intercepted.
- * @return {@link #CHANNEL_TYPE_UNSET}, {@link #CHANNEL_TYPE_PRIORITY}, or
- * {@link #CHANNEL_TYPE_NONE}
+ * Whether this policy allows {@link NotificationChannel channels} marked as
+ * {@link NotificationChannel#canBypassDnd()} to bypass DND. If {@link #STATE_ALLOW}, these
+ * channels may bypass; if {@link #STATE_DISALLOW}, then even notifications from channels
+ * with {@link NotificationChannel#canBypassDnd()} will be intercepted.
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- public @ChannelType int getAllowedChannels() {
- return mAllowChannels;
+ public @State int getPriorityChannels() {
+ switch (mAllowChannels) {
+ case CHANNEL_POLICY_PRIORITY:
+ return STATE_ALLOW;
+ case CHANNEL_POLICY_NONE:
+ return STATE_DISALLOW;
+ default:
+ return STATE_UNSET;
+ }
}
/**
@@ -628,8 +613,6 @@
* is not set, it is (@link STATE_UNSET} and will not change the current set policy.
*/
public static final class Builder {
- private @ModifiableField int mUserModifiedFields;
-
private ZenPolicy mZenPolicy;
public Builder() {
@@ -644,9 +627,6 @@
public Builder(@Nullable ZenPolicy policy) {
if (policy != null) {
mZenPolicy = policy.copy();
- if (Flags.modesApi()) {
- mUserModifiedFields = policy.mUserModifiedFields;
- }
} else {
mZenPolicy = new ZenPolicy();
}
@@ -657,11 +637,10 @@
*/
public @NonNull ZenPolicy build() {
if (Flags.modesApi()) {
- return new ZenPolicy(new ArrayList<Integer>(mZenPolicy.mPriorityCategories),
- new ArrayList<Integer>(mZenPolicy.mVisualEffects),
+ return new ZenPolicy(new ArrayList<>(mZenPolicy.mPriorityCategories),
+ new ArrayList<>(mZenPolicy.mVisualEffects),
mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls,
- mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels,
- mUserModifiedFields);
+ mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels);
} else {
return mZenPolicy.copy();
}
@@ -1016,32 +995,10 @@
* Set whether priority channels are permitted to break through DND.
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- public @NonNull Builder allowChannels(@ChannelType int channelType) {
- mZenPolicy.mAllowChannels = channelType;
+ public @NonNull Builder allowPriorityChannels(boolean allow) {
+ mZenPolicy.mAllowChannels = allow ? CHANNEL_POLICY_PRIORITY : CHANNEL_POLICY_NONE;
return this;
}
-
- /**
- * Sets the user modified fields bitmask.
- * @hide
- */
- @TestApi
- @FlaggedApi(Flags.FLAG_MODES_API)
- public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) {
- mUserModifiedFields = userModifiedFields;
- return this;
- }
- }
-
- /**
- Gets the bitmask representing which fields are user modified. Bits are set using
- * {@link ModifiableField}.
- * @hide
- */
- @TestApi
- @FlaggedApi(Flags.FLAG_MODES_API)
- public @ModifiableField int getUserModifiedFields() {
- return mUserModifiedFields;
}
@Override
@@ -1058,7 +1015,6 @@
dest.writeInt(mConversationSenders);
if (Flags.modesApi()) {
dest.writeInt(mAllowChannels);
- dest.writeInt(mUserModifiedFields);
}
}
@@ -1074,7 +1030,7 @@
trimList(source.readArrayList(Integer.class.getClassLoader(),
Integer.class), NUM_VISUAL_EFFECTS),
source.readInt(), source.readInt(), source.readInt(),
- source.readInt(), source.readInt()
+ source.readInt()
);
} else {
policy = new ZenPolicy();
@@ -1109,14 +1065,12 @@
conversationTypeToString(mConversationSenders));
if (Flags.modesApi()) {
sb.append(", allowChannels=").append(channelTypeToString(mAllowChannels));
- sb.append(", userModifiedFields=")
- .append(modifiedFieldsToString(mUserModifiedFields));
}
return sb.append('}').toString();
}
- @FlaggedApi(Flags.FLAG_MODES_API)
- private String modifiedFieldsToString(@ModifiableField int bitmask) {
+ /** @hide */
+ public static String fieldsToString(@ModifiableField int bitmask) {
ArrayList<String> modified = new ArrayList<>();
if ((bitmask & FIELD_MESSAGES) != 0) {
modified.add("FIELD_MESSAGES");
@@ -1305,11 +1259,11 @@
@FlaggedApi(Flags.FLAG_MODES_API)
public static String channelTypeToString(@ChannelType int channelType) {
switch (channelType) {
- case CHANNEL_TYPE_UNSET:
+ case CHANNEL_POLICY_UNSET:
return "unset";
- case CHANNEL_TYPE_PRIORITY:
+ case CHANNEL_POLICY_PRIORITY:
return "priority";
- case CHANNEL_TYPE_NONE:
+ case CHANNEL_POLICY_NONE:
return "none";
}
return "invalidChannelType{" + channelType + "}";
@@ -1327,8 +1281,7 @@
&& other.mPriorityMessages == mPriorityMessages
&& other.mConversationSenders == mConversationSenders;
if (Flags.modesApi()) {
- return eq && other.mAllowChannels == mAllowChannels
- && other.mUserModifiedFields == mUserModifiedFields;
+ return eq && other.mAllowChannels == mAllowChannels;
}
return eq;
}
@@ -1337,7 +1290,7 @@
public int hashCode() {
if (Flags.modesApi()) {
return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls,
- mPriorityMessages, mConversationSenders, mAllowChannels, mUserModifiedFields);
+ mPriorityMessages, mConversationSenders, mAllowChannels);
}
return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, mPriorityMessages,
mConversationSenders);
@@ -1389,11 +1342,11 @@
}
/** @hide */
- public boolean isCategoryAllowed(@PriorityCategory int category, boolean defaultVal) {
- switch (getZenPolicyPriorityCategoryState(category)) {
- case ZenPolicy.STATE_ALLOW:
+ public static boolean stateToBoolean(@State int state, boolean defaultVal) {
+ switch (state) {
+ case STATE_ALLOW:
return true;
- case ZenPolicy.STATE_DISALLOW:
+ case STATE_DISALLOW:
return false;
default:
return defaultVal;
@@ -1401,15 +1354,13 @@
}
/** @hide */
+ public boolean isCategoryAllowed(@PriorityCategory int category, boolean defaultVal) {
+ return stateToBoolean(getZenPolicyPriorityCategoryState(category), defaultVal);
+ }
+
+ /** @hide */
public boolean isVisualEffectAllowed(@VisualEffect int effect, boolean defaultVal) {
- switch (getZenPolicyVisualEffectState(effect)) {
- case ZenPolicy.STATE_ALLOW:
- return true;
- case ZenPolicy.STATE_DISALLOW:
- return false;
- default:
- return defaultVal;
- }
+ return stateToBoolean(getZenPolicyVisualEffectState(effect), defaultVal);
}
/**
@@ -1463,8 +1414,8 @@
// apply allowed channels
if (Flags.modesApi()) {
// if no channels are allowed, can't newly allow them
- if (mAllowChannels != CHANNEL_TYPE_NONE
- && policyToApply.mAllowChannels != CHANNEL_TYPE_UNSET) {
+ if (mAllowChannels != CHANNEL_POLICY_NONE
+ && policyToApply.mAllowChannels != CHANNEL_POLICY_UNSET) {
mAllowChannels = policyToApply.mAllowChannels;
}
}
@@ -1530,7 +1481,7 @@
proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, getPriorityConversationSenders());
if (Flags.modesApi()) {
- proto.write(DNDPolicyProto.ALLOW_CHANNELS, getAllowedChannels());
+ proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannels());
}
proto.flush();
diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java
index adc54f5..f2bdbf6 100644
--- a/core/java/android/service/voice/VisualQueryDetector.java
+++ b/core/java/android/service/voice/VisualQueryDetector.java
@@ -325,7 +325,7 @@
Slog.v(TAG, "BinderCallback#onQueryDetected");
Binder.withCleanCallingIdentity(() -> {
synchronized (mLock) {
- mCallback.onQueryDetected(partialQuery);
+ mExecutor.execute(()->mCallback.onQueryDetected(partialQuery));
}
});
}
@@ -335,7 +335,7 @@
Slog.v(TAG, "BinderCallback#onQueryFinished");
Binder.withCleanCallingIdentity(() -> {
synchronized (mLock) {
- mCallback.onQueryFinished();
+ mExecutor.execute(()->mCallback.onQueryFinished());
}
});
}
@@ -345,7 +345,7 @@
Slog.v(TAG, "BinderCallback#onQueryRejected");
Binder.withCleanCallingIdentity(() -> {
synchronized (mLock) {
- mCallback.onQueryRejected();
+ mExecutor.execute(()->mCallback.onQueryRejected());
}
});
}
diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java
index cf1156d..fb57921 100644
--- a/core/java/android/telephony/PhoneStateListener.java
+++ b/core/java/android/telephony/PhoneStateListener.java
@@ -1681,6 +1681,10 @@
@EmergencyCallbackModeStopReason int reason) {
// not support. Can't override. Use TelephonyCallback.
}
+
+ public final void onSimultaneousCallingStateChanged(int[] subIds) {
+ // not supported on the deprecated interface - Use TelephonyCallback instead
+ }
}
private void log(String s) {
diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java
index 19bcf28..dc6a035 100644
--- a/core/java/android/telephony/TelephonyCallback.java
+++ b/core/java/android/telephony/TelephonyCallback.java
@@ -18,6 +18,7 @@
import android.Manifest;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
@@ -33,15 +34,19 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.IPhoneStateListener;
+import com.android.internal.telephony.flags.Flags;
import dalvik.system.VMRuntime;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
/**
* A callback class for monitoring changes in specific telephony states
@@ -627,6 +632,18 @@
public static final int EVENT_EMERGENCY_CALLBACK_MODE_CHANGED = 40;
/**
+ * Event for listening to changes in simultaneous cellular calling subscriptions.
+ *
+ * @see SimultaneousCellularCallingSupportListener
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS)
+ @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @SystemApi
+ public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41;
+
+ /**
* @hide
*/
@IntDef(prefix = {"EVENT_"}, value = {
@@ -669,7 +686,8 @@
EVENT_LINK_CAPACITY_ESTIMATE_CHANGED,
EVENT_TRIGGER_NOTIFY_ANBR,
EVENT_MEDIA_QUALITY_STATUS_CHANGED,
- EVENT_EMERGENCY_CALLBACK_MODE_CHANGED
+ EVENT_EMERGENCY_CALLBACK_MODE_CHANGED,
+ EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED
})
@Retention(RetentionPolicy.SOURCE)
public @interface TelephonyEvent {
@@ -1373,6 +1391,44 @@
}
/**
+ * Interface for listening to changes in the simultaneous cellular calling state for active
+ * cellular subscriptions.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS)
+ @SystemApi
+ public interface SimultaneousCellularCallingSupportListener {
+ /**
+ * Notify the Listener that the subscriptions available for simultaneous <b>cellular</b>
+ * calling have changed.
+ * <p>
+ * If we have an ongoing <b>cellular</b> call on one subscription in this Set, a
+ * simultaneous incoming or outgoing <b>cellular</b> call is possible on any of the
+ * subscriptions in this Set. On a traditional Dual Sim Dual Standby device, simultaneous
+ * calling is not possible between subscriptions, where on a Dual Sim Dual Active device,
+ * simultaneous calling may be possible between subscriptions in certain network conditions.
+ * <p>
+ * Note: This listener only tracks the capability of the modem to perform simultaneous
+ * cellular calls and does not track the simultaneous calling state of scenarios based on
+ * multiple IMS registration over multiple transports (WiFi/Internet calling).
+ * <p>
+ * Note: This listener fires for all changes to cellular calling subscriptions independent
+ * of which subscription it is registered on.
+ *
+ * @param simultaneousCallingSubscriptionIds The Set of subscription IDs that support
+ * simultaneous calling. If there is an ongoing call on a subscription in this Set, then a
+ * simultaneous incoming or outgoing call is only possible for other subscriptions in this
+ * Set. If there is an ongoing call on a subscription that is not in this Set, then
+ * simultaneous calling is not possible at the current time.
+ *
+ */
+ @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ void onSimultaneousCellularCallingSubscriptionsChanged(
+ @NonNull Set<Integer> simultaneousCallingSubscriptionIds);
+ }
+
+ /**
* Interface for call attributes listener.
*
* @hide
@@ -1976,6 +2032,17 @@
allowedNetworkType)));
}
+ public void onSimultaneousCallingStateChanged(int[] subIds) {
+ SimultaneousCellularCallingSupportListener listener =
+ (SimultaneousCellularCallingSupportListener) mTelephonyCallbackWeakRef.get();
+ if (listener == null) return;
+
+ Binder.withCleanCallingIdentity(
+ () -> mExecutor.execute(
+ () -> listener.onSimultaneousCellularCallingSubscriptionsChanged(
+ Arrays.stream(subIds).boxed().collect(Collectors.toSet()))));
+ }
+
public void onLinkCapacityEstimateChanged(
List<LinkCapacityEstimate> linkCapacityEstimateList) {
LinkCapacityEstimateChangedListener listener =
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 886727e..0de4505 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -994,6 +994,21 @@
}
}
+ /**
+ * Notify external listeners that the subscriptions supporting simultaneous cellular calling
+ * have changed.
+ * @param subIds The new set of subIds supporting simultaneous cellular calling.
+ */
+ public void notifySimultaneousCellularCallingSubscriptionsChanged(Set<Integer> subIds) {
+ try {
+ sRegistry.notifySimultaneousCellularCallingSubscriptionsChanged(
+ subIds.stream().mapToInt(i -> i).toArray());
+ } catch (RemoteException ex) {
+ // system server crash
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
public @NonNull Set<Integer> getEventsFromCallback(
@NonNull TelephonyCallback telephonyCallback) {
Set<Integer> eventList = new ArraySet<>();
@@ -1135,7 +1150,11 @@
eventList.add(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED);
}
-
+ if (telephonyCallback
+ instanceof TelephonyCallback.SimultaneousCellularCallingSupportListener) {
+ eventList.add(
+ TelephonyCallback.EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED);
+ }
return eventList;
}
diff --git a/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java b/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java
new file mode 100644
index 0000000..819cc8c
--- /dev/null
+++ b/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 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.tracing.perfetto;
+
+import android.annotation.Nullable;
+
+/**
+ * @hide
+ * @param <DataSourceInstanceType> The type of datasource instance this state applied to.
+ */
+public class CreateIncrementalStateArgs<DataSourceInstanceType extends DataSourceInstance> {
+ private final DataSource<DataSourceInstanceType, Object, Object> mDataSource;
+ private final int mInstanceIndex;
+
+ CreateIncrementalStateArgs(DataSource dataSource, int instanceIndex) {
+ this.mDataSource = dataSource;
+ this.mInstanceIndex = instanceIndex;
+ }
+
+ /**
+ * Gets the datasource instance for this state with a lock.
+ * releaseDataSourceInstanceLocked must be called before this can be called again.
+ * @return The data source instance for this state.
+ * Null if the datasource instance no longer exists.
+ */
+ public @Nullable DataSourceInstanceType getDataSourceInstanceLocked() {
+ return mDataSource.getDataSourceInstanceLocked(mInstanceIndex);
+ }
+}
diff --git a/core/java/android/tracing/perfetto/CreateTlsStateArgs.java b/core/java/android/tracing/perfetto/CreateTlsStateArgs.java
new file mode 100644
index 0000000..3fad2d1
--- /dev/null
+++ b/core/java/android/tracing/perfetto/CreateTlsStateArgs.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 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.tracing.perfetto;
+
+import android.annotation.Nullable;
+
+/**
+ * @hide
+ * @param <DataSourceInstanceType> The type of datasource instance this state applied to.
+ */
+public class CreateTlsStateArgs<DataSourceInstanceType extends DataSourceInstance> {
+ private final DataSource<DataSourceInstanceType, Object, Object> mDataSource;
+ private final int mInstanceIndex;
+
+ CreateTlsStateArgs(DataSource dataSource, int instanceIndex) {
+ this.mDataSource = dataSource;
+ this.mInstanceIndex = instanceIndex;
+ }
+
+ /**
+ * Gets the datasource instance for this state with a lock.
+ * releaseDataSourceInstanceLocked must be called before this can be called again.
+ * @return The data source instance for this state.
+ * Null if the datasource instance no longer exists.
+ */
+ public @Nullable DataSourceInstanceType getDataSourceInstanceLocked() {
+ return mDataSource.getDataSourceInstanceLocked(mInstanceIndex);
+ }
+}
diff --git a/core/java/android/tracing/perfetto/DataSource.java b/core/java/android/tracing/perfetto/DataSource.java
new file mode 100644
index 0000000..4e08aee
--- /dev/null
+++ b/core/java/android/tracing/perfetto/DataSource.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 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.tracing.perfetto;
+
+import android.util.proto.ProtoInputStream;
+
+/**
+ * Templated base class meant to be derived by embedders to create a custom data
+ * source.
+ *
+ * @param <DataSourceInstanceType> The type for the DataSource instances that will be created from
+ * this DataSource type.
+ * @param <TlsStateType> The type of the custom TLS state, if any is used.
+ * @param <IncrementalStateType> The type of the custom incremental state, if any is used.
+ *
+ * @hide
+ */
+public abstract class DataSource<DataSourceInstanceType extends DataSourceInstance,
+ TlsStateType, IncrementalStateType> {
+ protected final long mNativeObj;
+
+ public final String name;
+
+ /**
+ * A function implemented by each datasource to create a new data source instance.
+ *
+ * @param configStream A ProtoInputStream to read the tracing instance's config.
+ * @return A new data source instance setup with the provided config.
+ */
+ public abstract DataSourceInstanceType createInstance(
+ ProtoInputStream configStream, int instanceIndex);
+
+ /**
+ * Constructor for datasource base class.
+ *
+ * @param name The fully qualified name of the datasource.
+ */
+ public DataSource(String name) {
+ this.name = name;
+ this.mNativeObj = nativeCreate(this, name);
+ }
+
+ /**
+ * The main tracing method. Tracing code should call this passing a lambda as
+ * argument, with the following signature: void(TraceContext).
+ * <p>
+ * The lambda will be called synchronously (i.e., always before trace()
+ * returns) only if tracing is enabled and the data source has been enabled in
+ * the tracing config.
+ * <p>
+ * The lambda can be called more than once per trace() call, in the case of
+ * concurrent tracing sessions (or even if the data source is instantiated
+ * twice within the same trace config).
+ *
+ * @param fun The tracing lambda that will be called with the tracing contexts of each active
+ * tracing instance.
+ */
+ public final void trace(
+ TraceFunction<DataSourceInstanceType, TlsStateType, IncrementalStateType> fun) {
+ nativeTrace(mNativeObj, fun);
+ }
+
+ /**
+ * Flush any trace data from this datasource that has not yet been flushed.
+ */
+ public final void flush() {
+ nativeFlushAll(mNativeObj);
+ }
+
+ /**
+ * Override this method to create a custom TlsState object for your DataSource. A new instance
+ * will be created per trace instance per thread.
+ *
+ * NOTE: Should only be called from native side.
+ */
+ protected TlsStateType createTlsState(CreateTlsStateArgs<DataSourceInstanceType> args) {
+ return null;
+ }
+
+ /**
+ * Override this method to create and use a custom IncrementalState object for your DataSource.
+ *
+ * NOTE: Should only be called from native side.
+ */
+ protected IncrementalStateType createIncrementalState(
+ CreateIncrementalStateArgs<DataSourceInstanceType> args) {
+ return null;
+ }
+
+ /**
+ * Registers the data source on all tracing backends, including ones that
+ * connect after the registration. Doing so enables the data source to receive
+ * Setup/Start/Stop notifications and makes the trace() method work when
+ * tracing is enabled and the data source is selected.
+ * <p>
+ * NOTE: Once registered, we cannot unregister the data source. Therefore, we should avoid
+ * creating and registering data source where not strictly required. This is a fundamental
+ * limitation of Perfetto itself.
+ *
+ * @param params Params to initialize the datasource with.
+ */
+ public void register(DataSourceParams params) {
+ nativeRegisterDataSource(this.mNativeObj, params.bufferExhaustedPolicy);
+ }
+
+ /**
+ * Gets the datasource instance with a specified index.
+ * IMPORTANT: releaseDataSourceInstance must be called after using the datasource instance.
+ * @param instanceIndex The index of the datasource to lock and get.
+ * @return The DataSourceInstance at index instanceIndex.
+ * Null if the datasource instance at the requested index doesn't exist.
+ */
+ public DataSourceInstanceType getDataSourceInstanceLocked(int instanceIndex) {
+ return (DataSourceInstanceType) nativeGetPerfettoInstanceLocked(mNativeObj, instanceIndex);
+ }
+
+ /**
+ * Unlock the datasource at the specified index.
+ * @param instanceIndex The index of the datasource to unlock.
+ */
+ protected void releaseDataSourceInstance(int instanceIndex) {
+ nativeReleasePerfettoInstanceLocked(mNativeObj, instanceIndex);
+ }
+
+ /**
+ * Called from native side when a new tracing instance starts.
+ *
+ * @param rawConfig byte array of the PerfettoConfig encoded proto.
+ * @return A new Java DataSourceInstance object.
+ */
+ private DataSourceInstanceType createInstance(byte[] rawConfig, int instanceIndex) {
+ final ProtoInputStream inputStream = new ProtoInputStream(rawConfig);
+ return this.createInstance(inputStream, instanceIndex);
+ }
+
+ private static native void nativeRegisterDataSource(
+ long dataSourcePtr, int bufferExhaustedPolicy);
+
+ private static native long nativeCreate(DataSource thiz, String name);
+ private static native void nativeTrace(
+ long nativeDataSourcePointer, TraceFunction traceFunction);
+ private static native void nativeFlushAll(long nativeDataSourcePointer);
+ private static native long nativeGetFinalizer();
+
+ private static native DataSourceInstance nativeGetPerfettoInstanceLocked(
+ long dataSourcePtr, int dsInstanceIdx);
+ private static native void nativeReleasePerfettoInstanceLocked(
+ long dataSourcePtr, int dsInstanceIdx);
+}
diff --git a/core/java/android/tracing/perfetto/DataSourceInstance.java b/core/java/android/tracing/perfetto/DataSourceInstance.java
new file mode 100644
index 0000000..4994501
--- /dev/null
+++ b/core/java/android/tracing/perfetto/DataSourceInstance.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 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.tracing.perfetto;
+
+/**
+ * @hide
+ */
+public abstract class DataSourceInstance implements AutoCloseable {
+ private final DataSource mDataSource;
+ private final int mInstanceIndex;
+
+ public DataSourceInstance(DataSource dataSource, int instanceIndex) {
+ this.mDataSource = dataSource;
+ this.mInstanceIndex = instanceIndex;
+ }
+
+ /**
+ * Executed when the tracing instance starts running.
+ * <p>
+ * NOTE: This callback executes on the Perfetto internal thread and is blocking.
+ * Anything that is run in this callback should execute quickly.
+ *
+ * @param args Start arguments.
+ */
+ protected void onStart(StartCallbackArguments args) {}
+
+ /**
+ * Executed when a flush is triggered.
+ * <p>
+ * NOTE: This callback executes on the Perfetto internal thread and is blocking.
+ * Anything that is run in this callback should execute quickly.
+ * @param args Flush arguments.
+ */
+ protected void onFlush(FlushCallbackArguments args) {}
+
+ /**
+ * Executed when the tracing instance is stopped.
+ * <p>
+ * NOTE: This callback executes on the Perfetto internal thread and is blocking.
+ * Anything that is run in this callback should execute quickly.
+ * @param args Stop arguments.
+ */
+ protected void onStop(StopCallbackArguments args) {}
+
+ @Override
+ public final void close() {
+ this.release();
+ }
+
+ /**
+ * Release the lock on the datasource once you are finished using it.
+ * Only required to be called when instance was retrieved with
+ * `DataSource#getDataSourceInstanceLocked`.
+ */
+ public final void release() {
+ mDataSource.releaseDataSourceInstance(mInstanceIndex);
+ }
+}
diff --git a/core/java/android/tracing/perfetto/DataSourceParams.java b/core/java/android/tracing/perfetto/DataSourceParams.java
new file mode 100644
index 0000000..6cd04e3
--- /dev/null
+++ b/core/java/android/tracing/perfetto/DataSourceParams.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 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.tracing.perfetto;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * DataSource Parameters
+ *
+ * @hide
+ */
+public class DataSourceParams {
+ /**
+ * @hide
+ */
+ @IntDef(value = {
+ PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP,
+ PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PerfettoDsBufferExhausted {}
+
+ // If the data source runs out of space when trying to acquire a new chunk,
+ // it will drop data.
+ public static final int PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP = 0;
+
+ // If the data source runs out of space when trying to acquire a new chunk,
+ // it will stall, retry and eventually abort if a free chunk is not acquired
+ // after a while.
+ public static final int PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT = 1;
+
+ public static DataSourceParams DEFAULTS =
+ new DataSourceParams(PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP);
+
+ public DataSourceParams(@PerfettoDsBufferExhausted int bufferExhaustedPolicy) {
+ this.bufferExhaustedPolicy = bufferExhaustedPolicy;
+ }
+
+ public final @PerfettoDsBufferExhausted int bufferExhaustedPolicy;
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/tracing/perfetto/FlushCallbackArguments.java
similarity index 81%
rename from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
rename to core/java/android/tracing/perfetto/FlushCallbackArguments.java
index ce92b6d..ecf6aee 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/tracing/perfetto/FlushCallbackArguments.java
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.companion.virtual.camera;
+
+package android.tracing.perfetto;
/**
- * The configuration of a single virtual camera stream.
* @hide
*/
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+public class FlushCallbackArguments {
+}
diff --git a/core/java/android/tracing/perfetto/InitArguments.java b/core/java/android/tracing/perfetto/InitArguments.java
new file mode 100644
index 0000000..da8c273
--- /dev/null
+++ b/core/java/android/tracing/perfetto/InitArguments.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 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.tracing.perfetto;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @hide
+ */
+public class InitArguments {
+ public final @PerfettoBackend int backends;
+
+ /**
+ * @hide
+ */
+ @IntDef(value = {
+ PERFETTO_BACKEND_IN_PROCESS,
+ PERFETTO_BACKEND_SYSTEM,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PerfettoBackend {}
+
+ // The in-process tracing backend. Keeps trace buffers in the process memory.
+ public static final int PERFETTO_BACKEND_IN_PROCESS = (1 << 0);
+
+ // The system tracing backend. Connects to the system tracing service (e.g.
+ // on Linux/Android/Mac uses a named UNIX socket).
+ public static final int PERFETTO_BACKEND_SYSTEM = (1 << 1);
+
+ public static InitArguments DEFAULTS = new InitArguments(PERFETTO_BACKEND_SYSTEM);
+
+ public static InitArguments TESTING = new InitArguments(PERFETTO_BACKEND_IN_PROCESS);
+
+ public InitArguments(@PerfettoBackend int backends) {
+ this.backends = backends;
+ }
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItem.java b/core/java/android/tracing/perfetto/Producer.java
similarity index 61%
copy from core/java/android/hardware/biometrics/PromptContentListItem.java
copy to core/java/android/tracing/perfetto/Producer.java
index fa3783d..a1b3eb7 100644
--- a/core/java/android/hardware/biometrics/PromptContentListItem.java
+++ b/core/java/android/tracing/perfetto/Producer.java
@@ -14,16 +14,21 @@
* limitations under the License.
*/
-package android.hardware.biometrics;
-
-import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
-
-import android.annotation.FlaggedApi;
+package android.tracing.perfetto;
/**
- * A list item shown on {@link PromptVerticalListContentView}.
+ * @hide
*/
-@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
-public interface PromptContentListItem {
-}
+public class Producer {
+ /**
+ * Initializes the global Perfetto producer.
+ *
+ * @param args arguments on how to initialize the Perfetto producer.
+ */
+ public static void init(InitArguments args) {
+ nativePerfettoProducerInit(args.backends);
+ }
+
+ private static native void nativePerfettoProducerInit(int backends);
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/tracing/perfetto/StartCallbackArguments.java
similarity index 81%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/tracing/perfetto/StartCallbackArguments.java
index ce92b6d..9739d27 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/tracing/perfetto/StartCallbackArguments.java
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.companion.virtual.camera;
+
+package android.tracing.perfetto;
/**
- * The configuration of a single virtual camera stream.
* @hide
*/
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+public class StartCallbackArguments {
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/tracing/perfetto/StopCallbackArguments.java
similarity index 81%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/tracing/perfetto/StopCallbackArguments.java
index ce92b6d..0cd1a18 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/tracing/perfetto/StopCallbackArguments.java
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.companion.virtual.camera;
+
+package android.tracing.perfetto;
/**
- * The configuration of a single virtual camera stream.
* @hide
*/
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+public class StopCallbackArguments {
+}
diff --git a/core/java/android/tracing/perfetto/TraceFunction.java b/core/java/android/tracing/perfetto/TraceFunction.java
new file mode 100644
index 0000000..62941df
--- /dev/null
+++ b/core/java/android/tracing/perfetto/TraceFunction.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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.tracing.perfetto;
+
+import java.io.IOException;
+
+/**
+ * The interface for the trace function called from native on a trace call with a context.
+ *
+ * @param <DataSourceInstanceType> The type of DataSource this tracing context is for.
+ * @param <TlsStateType> The type of the custom TLS state, if any is used.
+ * @param <IncrementalStateType> The type of the custom incremental state, if any is used.
+ *
+ * @hide
+ */
+public interface TraceFunction<DataSourceInstanceType extends DataSourceInstance,
+ TlsStateType, IncrementalStateType> {
+
+ /**
+ * This function will be called synchronously (i.e., always before trace() returns) only if
+ * tracing is enabled and the data source has been enabled in the tracing config.
+ * It can be called more than once per trace() call, in the case of concurrent tracing sessions
+ * (or even if the data source is instantiated twice within the same trace config).
+ *
+ * @param ctx the tracing context to trace for in the trace function.
+ */
+ void trace(TracingContext<DataSourceInstanceType, TlsStateType, IncrementalStateType> ctx)
+ throws IOException;
+}
diff --git a/core/java/android/tracing/perfetto/TracingContext.java b/core/java/android/tracing/perfetto/TracingContext.java
new file mode 100644
index 0000000..0bce26e
--- /dev/null
+++ b/core/java/android/tracing/perfetto/TracingContext.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 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.tracing.perfetto;
+
+import android.util.proto.ProtoOutputStream;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Argument passed to the lambda function passed to Trace().
+ *
+ * @param <DataSourceInstanceType> The type of the datasource this tracing context is for.
+ * @param <TlsStateType> The type of the custom TLS state, if any is used.
+ * @param <IncrementalStateType> The type of the custom incremental state, if any is used.
+ *
+ * @hide
+ */
+public class TracingContext<DataSourceInstanceType extends DataSourceInstance,
+ TlsStateType, IncrementalStateType> {
+
+ private final long mContextPtr;
+ private final TlsStateType mTlsState;
+ private final IncrementalStateType mIncrementalState;
+ private final List<ProtoOutputStream> mTracePackets = new ArrayList<>();
+
+ // Should only be created from the native side.
+ private TracingContext(long contextPtr, TlsStateType tlsState,
+ IncrementalStateType incrementalState) {
+ this.mContextPtr = contextPtr;
+ this.mTlsState = tlsState;
+ this.mIncrementalState = incrementalState;
+ }
+
+ /**
+ * Creates a new output stream to be used to write a trace packet to. The output stream will be
+ * encoded to the proto binary representation when the callback trace function finishes and
+ * send over to the native side to be included in the proto buffer.
+ *
+ * @return A proto output stream to write a trace packet proto to
+ */
+ public ProtoOutputStream newTracePacket() {
+ final ProtoOutputStream os = new ProtoOutputStream(0);
+ mTracePackets.add(os);
+ return os;
+ }
+
+ /**
+ * Forces a commit of the thread-local tracing data written so far to the
+ * service. This is almost never required (tracing data is periodically
+ * committed as trace pages are filled up) and has a non-negligible
+ * performance hit (requires an IPC + refresh of the current thread-local
+ * chunk). The only case when this should be used is when handling OnStop()
+ * asynchronously, to ensure sure that the data is committed before the
+ * Stop timeout expires.
+ */
+ public void flush() {
+ nativeFlush(this, mContextPtr);
+ }
+
+ /**
+ * Can optionally be used to store custom per-sequence
+ * session data, which is not reset when incremental state is cleared
+ * (e.g. configuration options).
+ *
+ * @return The TlsState instance for the tracing thread and instance.
+ */
+ public TlsStateType getCustomTlsState() {
+ return this.mTlsState;
+ }
+
+ /**
+ * Can optionally be used store custom per-sequence
+ * incremental data (e.g., interning tables).
+ *
+ * @return The current IncrementalState object instance.
+ */
+ public IncrementalStateType getIncrementalState() {
+ return this.mIncrementalState;
+ }
+
+ // Called from native to get trace packets
+ private byte[][] getAndClearAllPendingTracePackets() {
+ byte[][] res = new byte[mTracePackets.size()][];
+ for (int i = 0; i < mTracePackets.size(); i++) {
+ ProtoOutputStream tracePacket = mTracePackets.get(i);
+ res[i] = tracePacket.getBytes();
+ }
+
+ mTracePackets.clear();
+ return res;
+ }
+
+ // private static native void nativeFlush(long nativeDataSourcePointer);
+ private static native void nativeFlush(TracingContext thiz, long ctxPointer);
+}
diff --git a/core/java/android/tracing/transition/TransitionDataSource.java b/core/java/android/tracing/transition/TransitionDataSource.java
new file mode 100644
index 0000000..b8daace
--- /dev/null
+++ b/core/java/android/tracing/transition/TransitionDataSource.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 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.tracing.transition;
+
+import android.tracing.perfetto.DataSource;
+import android.tracing.perfetto.DataSourceInstance;
+import android.tracing.perfetto.FlushCallbackArguments;
+import android.tracing.perfetto.StartCallbackArguments;
+import android.tracing.perfetto.StopCallbackArguments;
+import android.util.proto.ProtoInputStream;
+
+/**
+ * @hide
+ */
+public class TransitionDataSource extends DataSource {
+ public static String DATA_SOURCE_NAME = "com.android.wm.shell.transition";
+
+ private final Runnable mOnStartStaticCallback;
+ private final Runnable mOnFlushStaticCallback;
+ private final Runnable mOnStopStaticCallback;
+
+ public TransitionDataSource(Runnable onStart, Runnable onFlush, Runnable onStop) {
+ super(DATA_SOURCE_NAME);
+ this.mOnStartStaticCallback = onStart;
+ this.mOnFlushStaticCallback = onFlush;
+ this.mOnStopStaticCallback = onStop;
+ }
+
+ @Override
+ public DataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) {
+ return new DataSourceInstance(this, instanceIndex) {
+ @Override
+ protected void onStart(StartCallbackArguments args) {
+ mOnStartStaticCallback.run();
+ }
+
+ @Override
+ protected void onFlush(FlushCallbackArguments args) {
+ mOnFlushStaticCallback.run();
+ }
+
+ @Override
+ protected void onStop(StopCallbackArguments args) {
+ mOnStopStaticCallback.run();
+ }
+ };
+ }
+}
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 36b74e3..7903050 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -69,12 +69,13 @@
import android.view.displayhash.DisplayHash;
import android.view.displayhash.VerifiedDisplayHash;
import android.window.AddToSurfaceSyncGroupResult;
+import android.window.IScreenRecordingCallback;
import android.window.ISurfaceSyncGroupCompletedListener;
import android.window.ITaskFpsCallback;
-import android.window.ScreenCapture;
-import android.window.WindowContextInfo;
import android.window.ITrustedPresentationListener;
+import android.window.ScreenCapture;
import android.window.TrustedPresentationThresholds;
+import android.window.WindowContextInfo;
/**
* System private interface to the window manager.
@@ -1083,4 +1084,8 @@
void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id);
+
+ boolean registerScreenRecordingCallback(IScreenRecordingCallback callback);
+
+ void unregisterScreenRecordingCallback(IScreenRecordingCallback callback);
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 257ecc5..c98d1d7 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -15015,6 +15015,7 @@
}
/** @hide */
+ @Nullable
View getSelfOrParentImportantForA11y() {
if (isImportantForAccessibility()) return this;
ViewParent parent = getParentForAccessibility();
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index c66f3c8..34b46700 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -87,6 +87,7 @@
import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW;
import static android.view.WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS;
import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED;
+import static android.view.accessibility.Flags.fixMergedContentChangeEvent;
import static android.view.accessibility.Flags.forceInvertColor;
import static android.view.accessibility.Flags.reduceWindowContentChangedEventThrottle;
import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER;
@@ -11509,6 +11510,15 @@
event.setContentChangeTypes(mChangeTypes);
if (mAction.isPresent()) event.setAction(mAction.getAsInt());
if (AccessibilityEvent.DEBUG_ORIGIN) event.originStackTrace = mOrigin;
+
+ if (fixMergedContentChangeEvent()) {
+ if ((mChangeTypes & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
+ final View importantParent = source.getSelfOrParentImportantForA11y();
+ if (importantParent != null) {
+ source = importantParent;
+ }
+ }
+ }
source.sendAccessibilityEventUnchecked(event);
} else {
mLastEventTimeMillis = 0;
@@ -11543,14 +11553,29 @@
}
if (mSource != null) {
- // If there is no common predecessor, then mSource points to
- // a removed view, hence in this case always prefer the source.
- View predecessor = getCommonPredecessor(mSource, source);
- if (predecessor != null) {
- predecessor = predecessor.getSelfOrParentImportantForA11y();
+ if (fixMergedContentChangeEvent()) {
+ View newSource = getCommonPredecessor(mSource, source);
+ if (newSource == null) {
+ // If there is no common predecessor, then mSource points to
+ // a removed view, hence in this case always prefer the source.
+ newSource = source;
+ }
+
+ mChangeTypes |= changeType;
+ if (mSource != newSource) {
+ mChangeTypes |= AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE;
+ mSource = newSource;
+ }
+ } else {
+ // If there is no common predecessor, then mSource points to
+ // a removed view, hence in this case always prefer the source.
+ View predecessor = getCommonPredecessor(mSource, source);
+ if (predecessor != null) {
+ predecessor = predecessor.getSelfOrParentImportantForA11y();
+ }
+ mSource = (predecessor != null) ? predecessor : source;
+ mChangeTypes |= changeType;
}
- mSource = (predecessor != null) ? predecessor : source;
- mChangeTypes |= changeType;
final int performingAction = mAccessibilityManager.getPerformingAction();
if (performingAction != 0) {
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index c7355c1..efae57c 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -53,6 +53,13 @@
flag {
namespace: "accessibility"
+ name: "fix_merged_content_change_event"
+ description: "Fixes event type and source of content change event merged in ViewRootImpl"
+ bug: "277305460"
+}
+
+flag {
+ namespace: "accessibility"
name: "flash_notification_system_api"
description: "Makes flash notification APIs as system APIs for calling from mainline module"
bug: "303131332"
diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java
index 828ec26..c6271d2 100644
--- a/core/java/android/webkit/URLUtil.java
+++ b/core/java/android/webkit/URLUtil.java
@@ -16,20 +16,40 @@
package android.webkit;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.compat.Compatibility;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.ParseException;
import android.net.Uri;
import android.net.WebAddress;
+import android.os.Build;
import android.util.Log;
import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.Charset;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class URLUtil {
+ /**
+ * This feature enables parsing of Content-Disposition headers that conform to RFC 6266. In
+ * particular, this enables parsing of {@code filename*} values which can use a different
+ * character encoding.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @FlaggedApi(android.os.Flags.FLAG_ANDROID_OS_BUILD_VANILLA_ICE_CREAM)
+ public static final long PARSE_CONTENT_DISPOSITION_USING_RFC_6266 = 319400769L;
+
private static final String LOGTAG = "webkit";
private static final boolean TRACE = false;
@@ -293,21 +313,58 @@
/**
* Guesses canonical filename that a download would have, using the URL and contentDisposition.
- * File extension, if not defined, is added based on the mimetype
+ *
+ * <p>File extension, if not defined, is added based on the mimetype.
+ *
+ * <p>The {@code contentDisposition} argument will be treated differently depending on
+ * targetSdkVersion.
+ *
+ * <ul>
+ * <li>For targetSDK versions < {@code VANILLA_ICE_CREAM} it will be parsed based on RFC
+ * 2616.
+ * <li>For targetSDK versions >= {@code VANILLA_ICE_CREAM} it will be parsed based on RFC
+ * 6266.
+ * </ul>
+ *
+ * In practice, this means that from {@code VANILLA_ICE_CREAM}, this method will be able to
+ * parse {@code filename*} directives in the {@code contentDisposition} string.
+ *
+ * <p>The function also changed in the following ways in {@code VANILLA_ICE_CREAM}:
+ *
+ * <ul>
+ * <li>If the suggested file type extension doesn't match the passed {@code mimeType}, the
+ * method will append the appropriate extension instead of replacing the current
+ * extension.
+ * <li>If the suggested file name contains a path separator ({@code "/"}), the method will
+ * replace this with the underscore character ({@code "_"}) instead of splitting the
+ * result and only using the last part.
+ * </ul>
*
* @param url Url to the content
* @param contentDisposition Content-Disposition HTTP header or {@code null}
* @param mimeType Mime-type of the content or {@code null}
* @return suggested filename
*/
- public static final String guessFileName(
+ public static String guessFileName(
+ String url, @Nullable String contentDisposition, @Nullable String mimeType) {
+ if (android.os.Flags.androidOsBuildVanillaIceCream()) {
+ if (Compatibility.isChangeEnabled(PARSE_CONTENT_DISPOSITION_USING_RFC_6266)) {
+ return guessFileNameRfc6266(url, contentDisposition, mimeType);
+ }
+ }
+
+ return guessFileNameRfc2616(url, contentDisposition, mimeType);
+ }
+
+ /** Legacy implementation of guessFileName, based on RFC 2616. */
+ private static String guessFileNameRfc2616(
String url, @Nullable String contentDisposition, @Nullable String mimeType) {
String filename = null;
String extension = null;
// If we couldn't do anything with the hint, move toward the content disposition
if (contentDisposition != null) {
- filename = parseContentDisposition(contentDisposition);
+ filename = parseContentDispositionRfc2616(contentDisposition);
if (filename != null) {
int index = filename.lastIndexOf('/') + 1;
if (index > 0) {
@@ -384,6 +441,128 @@
return filename + extension;
}
+ /**
+ * Guesses canonical filename that a download would have, using the URL and contentDisposition.
+ * Uses RFC 6266 for parsing the contentDisposition header value.
+ */
+ @NonNull
+ private static String guessFileNameRfc6266(
+ @NonNull String url, @Nullable String contentDisposition, @Nullable String mimeType) {
+ String filename = getFilenameSuggestion(url, contentDisposition);
+ // Split filename between base and extension
+ // Add an extension if filename does not have one
+ String extensionFromMimeType = suggestExtensionFromMimeType(mimeType);
+
+ if (filename.indexOf('.') < 0) {
+ // Filename does not have an extension, use the suggested one.
+ return filename + extensionFromMimeType;
+ }
+
+ // Filename already contains at least one dot.
+ // Compare the last segment of the extension against the mime type.
+ // If there's a mismatch, add the suggested extension instead.
+ if (mimeType != null && extensionDifferentFromMimeType(filename, mimeType)) {
+ return filename + extensionFromMimeType;
+ }
+ return filename;
+ }
+
+ /**
+ * Get the suggested file name from the {@code contentDisposition} or {@code url}. Will ensure
+ * that the filename contains no path separators by replacing them with the {@code "_"}
+ * character.
+ */
+ @NonNull
+ private static String getFilenameSuggestion(String url, @Nullable String contentDisposition) {
+ // First attempt to parse the Content-Disposition header if available
+ if (contentDisposition != null) {
+ String filename = getFilenameFromContentDispositionRfc6266(contentDisposition);
+ if (filename != null) {
+ return replacePathSeparators(filename);
+ }
+ }
+
+ // Try to generate a filename based on the URL.
+ if (url != null) {
+ Uri parsedUri = Uri.parse(url);
+ String lastPathSegment = parsedUri.getLastPathSegment();
+ if (lastPathSegment != null) {
+ return replacePathSeparators(lastPathSegment);
+ }
+ }
+
+ // Finally, if couldn't get filename from URI, get a generic filename.
+ return "downloadfile";
+ }
+
+ /**
+ * Replace all instances of {@code "/"} with {@code "_"} to avoid filenames that navigate the
+ * path.
+ */
+ @NonNull
+ private static String replacePathSeparators(@NonNull String raw) {
+ return raw.replaceAll("/", "_");
+ }
+
+ /**
+ * Check if the {@code filename} has an extension that is different from the expected one based
+ * on the {@code mimeType}.
+ */
+ private static boolean extensionDifferentFromMimeType(
+ @NonNull String filename, @NonNull String mimeType) {
+ int lastDotIndex = filename.lastIndexOf('.');
+ String typeFromExt =
+ MimeTypeMap.getSingleton()
+ .getMimeTypeFromExtension(filename.substring(lastDotIndex + 1));
+ return typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType);
+ }
+
+ /**
+ * Get a candidate file extension (including the {@code .}) for the given mimeType. will return
+ * {@code ".bin"} if {@code mimeType} is {@code null}
+ *
+ * @param mimeType Reported mimetype
+ * @return A file extension, including the {@code .}
+ */
+ @NonNull
+ private static String suggestExtensionFromMimeType(@Nullable String mimeType) {
+ if (mimeType == null) {
+ return ".bin";
+ }
+ String extensionFromMimeType =
+ MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+ if (extensionFromMimeType != null) {
+ return "." + extensionFromMimeType;
+ }
+ if (mimeType.equalsIgnoreCase("text/html")) {
+ return ".html";
+ } else if (mimeType.toLowerCase(Locale.ROOT).startsWith("text/")) {
+ return ".txt";
+ } else {
+ return ".bin";
+ }
+ }
+
+ /**
+ * Parse the Content-Disposition HTTP Header.
+ *
+ * <p>Behavior depends on targetSdkVersion.
+ *
+ * <ul>
+ * <li>For targetSDK versions < {@code VANILLA_ICE_CREAM} it will parse based on RFC 2616.
+ * <li>For targetSDK versions >= {@code VANILLA_ICE_CREAM} it will parse based on RFC 6266.
+ * </ul>
+ */
+ @UnsupportedAppUsage
+ static String parseContentDisposition(String contentDisposition) {
+ if (android.os.Flags.androidOsBuildVanillaIceCream()) {
+ if (Compatibility.isChangeEnabled(PARSE_CONTENT_DISPOSITION_USING_RFC_6266)) {
+ return getFilenameFromContentDispositionRfc6266(contentDisposition);
+ }
+ }
+ return parseContentDispositionRfc2616(contentDisposition);
+ }
+
/** Regex used to parse content-disposition headers */
private static final Pattern CONTENT_DISPOSITION_PATTERN =
Pattern.compile(
@@ -391,15 +570,14 @@
Pattern.CASE_INSENSITIVE);
/**
- * Parse the Content-Disposition HTTP Header. The format of the header is defined here:
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for
- * content that is going to be downloaded to the file system. We only support the attachment
- * type. Note that RFC 2616 specifies the filename value must be double-quoted. Unfortunately
- * some servers do not quote the value so to maintain consistent behaviour with other browsers,
- * we allow unquoted values too.
+ * Parse the Content-Disposition HTTP Header. The format of the header is defined here: <a
+ * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html">rfc2616 Section 19</a>. This
+ * header provides a filename for content that is going to be downloaded to the file system. We
+ * only support the attachment type. Note that RFC 2616 specifies the filename value must be
+ * double-quoted. Unfortunately some servers do not quote the value so to maintain consistent
+ * behaviour with other browsers, we allow unquoted values too.
*/
- @UnsupportedAppUsage
- static String parseContentDisposition(String contentDisposition) {
+ private static String parseContentDispositionRfc2616(String contentDisposition) {
try {
Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
if (m.find()) {
@@ -410,4 +588,136 @@
}
return null;
}
+
+ /**
+ * Pattern for parsing individual content disposition key-value pairs.
+ *
+ * <p>The pattern will attempt to parse the value as either single-, double-, or unquoted. For
+ * the single- and double-quoted options, the pattern allows escaped quotes as part of the
+ * value, as per <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-2.2">rfc2616
+ * section-2.2</a>
+ */
+ @SuppressWarnings("RegExpRepeatedSpace") // Spaces are only for readability.
+ private static final Pattern DISPOSITION_PATTERN =
+ Pattern.compile(
+ """
+ \\s*(\\S+?) # Group 1: parameter name
+ \\s*=\\s* # Match equals sign
+ (?: # non-capturing group of options
+ '( (?: [^'\\\\] | \\\\. )* )' # Group 2: single-quoted
+ | "( (?: [^"\\\\] | \\\\. )* )" # Group 3: double-quoted
+ | ( [^'"][^;\\s]* ) # Group 4: un-quoted parameter
+ )\\s*;? # Optional end semicolon""",
+ Pattern.COMMENTS);
+
+ /**
+ * Extract filename from a {@code Content-Disposition} header value.
+ *
+ * <p>This method implements the parsing defined in <a
+ * href="https://datatracker.ietf.org/doc/html/rfc6266">RFC 6266</a>, supporting both the {@code
+ * filename} and {@code filename*} disposition parameters. If the passed header value has the
+ * {@code "inline"} disposition type, this method will return {@code null} to indicate that a
+ * download was not intended.
+ *
+ * <p>If both {@code filename*} and {@code filename} is present, the former will be returned, as
+ * per the RFC. Invalid encoded values will be ignored.
+ *
+ * @param contentDisposition Value of {@code Content-Disposition} header.
+ * @return The filename suggested by the header or {@code null} if no filename could be parsed
+ * from the header value.
+ */
+ @Nullable
+ private static String getFilenameFromContentDispositionRfc6266(
+ @NonNull String contentDisposition) {
+ String[] parts = contentDisposition.trim().split(";", 2);
+ if (parts.length < 2) {
+ // Need at least 2 parts, the `disposition-type` and at least one `disposition-parm`.
+ return null;
+ }
+ String dispositionType = parts[0].trim();
+ if ("inline".equalsIgnoreCase(dispositionType)) {
+ // "inline" should not result in a download.
+ // Unknown disposition types should be handles as "attachment"
+ // https://datatracker.ietf.org/doc/html/rfc6266#section-4.2
+ return null;
+ }
+ String dispositionParameters = parts[1];
+ Matcher matcher = DISPOSITION_PATTERN.matcher(dispositionParameters);
+ String filename = null;
+ String filenameExt = null;
+ while (matcher.find()) {
+ String parameter = matcher.group(1);
+ String value;
+ if (matcher.group(2) != null) {
+ value = removeSlashEscapes(matcher.group(2)); // Value was single-quoted
+ } else if (matcher.group(3) != null) {
+ value = removeSlashEscapes(matcher.group(3)); // Value was double-quoted
+ } else {
+ value = matcher.group(4); // Value was un-quoted
+ }
+
+ if (parameter == null || value == null) {
+ continue;
+ }
+
+ if ("filename*".equalsIgnoreCase(parameter)) {
+ filenameExt = parseExtValueString(value);
+ } else if ("filename".equalsIgnoreCase(parameter)) {
+ filename = value;
+ }
+ }
+
+ // RFC 6266 dictates the filenameExt should be preferred if present.
+ if (filenameExt != null) {
+ return filenameExt;
+ }
+ return filename;
+ }
+
+ /** Replace escapes of the \X form with X. */
+ private static String removeSlashEscapes(String raw) {
+ if (raw == null) {
+ return null;
+ }
+ return raw.replaceAll("\\\\(.)", "$1");
+ }
+
+ /**
+ * Parse an extended value string which can be percent-encoded. Return {@code} null if unable to
+ * parse the string.
+ */
+ private static String parseExtValueString(String raw) {
+ String[] parts = raw.split("'", 3);
+ if (parts.length < 3) {
+ return null;
+ }
+
+ String encoding = parts[0];
+ // Intentionally ignore parts[1] (language).
+ String valueChars = parts[2];
+
+ try {
+ // The URLDecoder force-decodes + as " "
+ // so preemptively replace all values with the encoded value to preserve them.
+ Charset charset = Charset.forName(encoding);
+ String valueWithEncodedPlus = encodePlusCharacters(valueChars, charset);
+ return URLDecoder.decode(valueWithEncodedPlus, charset);
+ } catch (RuntimeException ignored) {
+ return null; // Ignoring an un-parsable value is within spec.
+ }
+ }
+
+ /**
+ * Replace all instances of {@code "+"} with the percent-encoded equivalent for the given {@code
+ * charset}.
+ */
+ @NonNull
+ private static String encodePlusCharacters(@NonNull String valueChars, Charset charset) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : charset.encode("+").array()) {
+ // Formatting a byte is not possible with TextUtils.formatSimple
+ sb.append(String.format("%02x", b));
+ }
+ return valueChars.replaceAll("\\+", sb.toString());
+ }
}
diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java
index 0ef37d1..53b047a 100644
--- a/core/java/android/webkit/WebViewFactory.java
+++ b/core/java/android/webkit/WebViewFactory.java
@@ -16,6 +16,8 @@
package android.webkit;
+import static android.webkit.Flags.updateServiceV2;
+
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.annotation.UptimeMillisLong;
@@ -33,6 +35,7 @@
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.Trace;
+import android.text.TextUtils;
import android.util.AndroidRuntimeException;
import android.util.ArraySet;
import android.util.Log;
@@ -410,6 +413,21 @@
}
}
+ // Returns whether the given package is enabled.
+ // This state can be changed by the user from Settings->Apps
+ private static boolean isEnabledPackage(PackageInfo packageInfo) {
+ if (packageInfo == null) return false;
+ return packageInfo.applicationInfo.enabled;
+ }
+
+ // Return {@code true} if the package is installed and not hidden
+ private static boolean isInstalledPackage(PackageInfo packageInfo) {
+ if (packageInfo == null) return false;
+ return (((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0)
+ && ((packageInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_HIDDEN)
+ == 0));
+ }
+
@UnsupportedAppUsage
private static Context getWebViewContextAndSetProvider() throws MissingWebViewPackageException {
Application initialApplication = AppGlobals.getInitialApplication();
@@ -456,6 +474,21 @@
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
+ if (updateServiceV2() && !isInstalledPackage(newPackageInfo)) {
+ throw new MissingWebViewPackageException(
+ TextUtils.formatSimple(
+ "Current WebView Package (%s) is not installed for the current "
+ + "user",
+ newPackageInfo.packageName));
+ }
+
+ if (updateServiceV2() && !isEnabledPackage(newPackageInfo)) {
+ throw new MissingWebViewPackageException(
+ TextUtils.formatSimple(
+ "Current WebView Package (%s) is not enabled for the current user",
+ newPackageInfo.packageName));
+ }
+
// Validate the newly fetched package info, throws MissingWebViewPackageException on
// failure
verifyPackageInfo(response.packageInfo, newPackageInfo);
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/window/IScreenRecordingCallback.aidl
similarity index 74%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to core/java/android/window/IScreenRecordingCallback.aidl
index ce92b6d..560ee75 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/window/IScreenRecordingCallback.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2024 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.
@@ -13,10 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.companion.virtual.camera;
+
+package android.window;
/**
- * The configuration of a single virtual camera stream.
* @hide
*/
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+oneway interface IScreenRecordingCallback {
+ void onScreenRecordingStateChanged(boolean visibleInScreenRecording);
+}
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
index acc6a74..7b8cdff 100644
--- a/core/java/android/window/TaskFragmentOperation.java
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -125,6 +125,16 @@
*/
public static final int OP_TYPE_SET_DIM_ON_TASK = 16;
+ /**
+ * Sets this TaskFragment to move to bottom of the Task if any of the activities below it is
+ * launched in a mode requiring clear top.
+ *
+ * This is only allowed for system organizers. See
+ * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer(
+ * ITaskFragmentOrganizer, boolean)}
+ */
+ public static final int OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH = 17;
+
@IntDef(prefix = { "OP_TYPE_" }, value = {
OP_TYPE_UNKNOWN,
OP_TYPE_CREATE_TASK_FRAGMENT,
@@ -144,6 +154,7 @@
OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE,
OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE,
OP_TYPE_SET_DIM_ON_TASK,
+ OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH,
})
@Retention(RetentionPolicy.SOURCE)
public @interface OperationType {}
@@ -173,12 +184,14 @@
private final boolean mDimOnTask;
+ private final boolean mMoveToBottomIfClearWhenLaunch;
+
private TaskFragmentOperation(@OperationType int opType,
@Nullable TaskFragmentCreationParams taskFragmentCreationParams,
@Nullable IBinder activityToken, @Nullable Intent activityIntent,
@Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken,
@Nullable TaskFragmentAnimationParams animationParams,
- boolean isolatedNav, boolean dimOnTask) {
+ boolean isolatedNav, boolean dimOnTask, boolean moveToBottomIfClearWhenLaunch) {
mOpType = opType;
mTaskFragmentCreationParams = taskFragmentCreationParams;
mActivityToken = activityToken;
@@ -188,6 +201,7 @@
mAnimationParams = animationParams;
mIsolatedNav = isolatedNav;
mDimOnTask = dimOnTask;
+ mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch;
}
private TaskFragmentOperation(Parcel in) {
@@ -200,6 +214,7 @@
mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR);
mIsolatedNav = in.readBoolean();
mDimOnTask = in.readBoolean();
+ mMoveToBottomIfClearWhenLaunch = in.readBoolean();
}
@Override
@@ -213,6 +228,7 @@
dest.writeTypedObject(mAnimationParams, flags);
dest.writeBoolean(mIsolatedNav);
dest.writeBoolean(mDimOnTask);
+ dest.writeBoolean(mMoveToBottomIfClearWhenLaunch);
}
@NonNull
@@ -300,6 +316,14 @@
return mDimOnTask;
}
+ /**
+ * Returns whether the TaskFragment should move to bottom of task when any activity below it
+ * is launched in clear top mode.
+ */
+ public boolean isMoveToBottomIfClearWhenLaunch() {
+ return mMoveToBottomIfClearWhenLaunch;
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
@@ -324,6 +348,7 @@
}
sb.append(", isolatedNav=").append(mIsolatedNav);
sb.append(", dimOnTask=").append(mDimOnTask);
+ sb.append(", moveToBottomIfClearWhenLaunch=").append(mMoveToBottomIfClearWhenLaunch);
sb.append('}');
return sb.toString();
@@ -332,7 +357,8 @@
@Override
public int hashCode() {
return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent,
- mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask);
+ mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask,
+ mMoveToBottomIfClearWhenLaunch);
}
@Override
@@ -349,7 +375,8 @@
&& Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken)
&& Objects.equals(mAnimationParams, other.mAnimationParams)
&& mIsolatedNav == other.mIsolatedNav
- && mDimOnTask == other.mDimOnTask;
+ && mDimOnTask == other.mDimOnTask
+ && mMoveToBottomIfClearWhenLaunch == other.mMoveToBottomIfClearWhenLaunch;
}
@Override
@@ -385,6 +412,8 @@
private boolean mDimOnTask;
+ private boolean mMoveToBottomIfClearWhenLaunch;
+
/**
* @param opType the {@link OperationType} of this {@link TaskFragmentOperation}.
*/
@@ -466,13 +495,23 @@
}
/**
+ * Sets whether the TaskFragment should move to bottom of task when any activity below it
+ * is launched in clear top mode.
+ */
+ @NonNull
+ public Builder setMoveToBottomIfClearWhenLaunch(boolean moveToBottomIfClearWhenLaunch) {
+ mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch;
+ return this;
+ }
+
+ /**
* Constructs the {@link TaskFragmentOperation}.
*/
@NonNull
public TaskFragmentOperation build() {
return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken,
mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams,
- mIsolatedNav, mDimOnTask);
+ mIsolatedNav, mDimOnTask, mMoveToBottomIfClearWhenLaunch);
}
}
}
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
index 4b5595f..cbf6367 100644
--- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -43,3 +43,10 @@
description: "Whether the API PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT is available"
bug: "310816437"
}
+
+flag {
+ name: "allow_hide_scm_button"
+ namespace: "large_screen_experiences_app_compat"
+ description: "Whether we should allow hiding the size compat restart button"
+ bug: "318840081"
+}
diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig
index 3c3c846..751c1a8 100644
--- a/core/java/android/window/flags/window_surfaces.aconfig
+++ b/core/java/android/window/flags/window_surfaces.aconfig
@@ -80,3 +80,11 @@
is_fixed_read_only: true
bug: "278757236"
}
+
+flag {
+ namespace: "window_surfaces"
+ name: "screen_recording_callbacks"
+ description: "Enable screen recording callbacks public API"
+ is_fixed_read_only: true
+ bug: "304574518"
+}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index bb16ad2..2b96ee6 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -69,11 +69,10 @@
}
flag {
- name: "predictive_back_system_animations"
+ name: "predictive_back_system_anims"
namespace: "systemui"
description: "Predictive back for system animations"
- bug: "319421778"
- is_fixed_read_only: true
+ bug: "320510464"
}
flag {
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
index 12aff1c..5d82d04 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
@@ -29,7 +29,6 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
-import android.multiuser.Flags;
import android.os.Build;
import android.os.PatternMatcher;
import android.util.Slog;
@@ -127,10 +126,6 @@
.setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SINGLE_USER,
R.styleable.AndroidManifestProvider_singleUser, sa));
- if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
- provider.setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SYSTEM_USER_ONLY,
- R.styleable.AndroidManifestProvider_systemUserOnly, sa));
- }
visibleToEphemeral = sa.getBoolean(
R.styleable.AndroidManifestProvider_visibleToInstantApps, false);
if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
index 4ac542f8..a1dd19a3 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
@@ -29,7 +29,6 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
-import android.multiuser.Flags;
import android.os.Build;
import com.android.internal.R;
@@ -106,11 +105,6 @@
| flag(ServiceInfo.FLAG_SINGLE_USER,
R.styleable.AndroidManifestService_singleUser, sa)));
- if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
- service.setFlags(service.getFlags() | flag(ServiceInfo.FLAG_SYSTEM_USER_ONLY,
- R.styleable.AndroidManifestService_systemUserOnly, sa));
- }
-
visibleToEphemeral = sa.getBoolean(
R.styleable.AndroidManifestService_visibleToInstantApps, false);
if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
index 03cfd4f..969f95d 100644
--- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
+++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
@@ -80,4 +80,5 @@
void onMediaQualityStatusChanged(in MediaQualityStatus mediaQualityStatus);
void onCallBackModeStarted(int type);
void onCallBackModeStopped(int type, int reason);
+ void onSimultaneousCallingStateChanged(in int[] subIds);
}
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index aab2242..0203ea4 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -104,6 +104,7 @@
void notifyAllowedNetworkTypesChanged(in int phoneId, in int subId, in int reason, in long allowedNetworkType);
void notifyLinkCapacityEstimateChanged(in int phoneId, in int subId,
in List<LinkCapacityEstimate> linkCapacityEstimateList);
+ void notifySimultaneousCellularCallingSubscriptionsChanged(in int[] subIds);
void addCarrierPrivilegesCallback(
int phoneId, ICarrierPrivilegesCallback callback, String pkg, String featureId);
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index 58ee2b2..13efaf0 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -26,6 +26,8 @@
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FACE_WAKE_AND_UNLOCK;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FINGERPRINT_WAKE_AND_UNLOCK;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK;
@@ -234,6 +236,19 @@
*/
public static final int ACTION_BACK_SYSTEM_ANIMATION = 25;
+ /**
+ * Time notifications spent in hidden state for performance reasons. We might temporary
+ * hide notifications after display size changes (e.g. fold/unfold of a foldable device)
+ * and measure them while they are hidden to unblock rendering of the rest of the UI.
+ */
+ public static final int ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE = 26;
+
+ /**
+ * The same as {@link ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE} but tracks time only
+ * when the notifications are hidden and when the shade is open or keyguard is visible.
+ */
+ public static final int ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN = 27;
+
private static final int[] ACTIONS_ALL = {
ACTION_EXPAND_PANEL,
ACTION_TOGGLE_RECENTS,
@@ -261,6 +276,8 @@
ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
ACTION_BACK_SYSTEM_ANIMATION,
+ ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
+ ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
};
/** @hide */
@@ -291,6 +308,8 @@
ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
ACTION_BACK_SYSTEM_ANIMATION,
+ ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
+ ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Action {
@@ -324,6 +343,8 @@
UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION,
+ UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
+ UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
};
private final Object mLock = new Object();
@@ -514,6 +535,10 @@
return "ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME";
case UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION:
return "ACTION_BACK_SYSTEM_ANIMATION";
+ case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE:
+ return "ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE";
+ case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN:
+ return "ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN";
default:
throw new IllegalArgumentException("Invalid action");
}
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 2a744e3..c8fd246 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -269,6 +269,9 @@
"android_window_WindowInfosListener.cpp",
"android_window_ScreenCapture.cpp",
"jni_common.cpp",
+ "android_tracing_PerfettoDataSource.cpp",
+ "android_tracing_PerfettoDataSourceInstance.cpp",
+ "android_tracing_PerfettoProducer.cpp",
],
static_libs: [
@@ -282,6 +285,7 @@
"libscrypt_static",
"libstatssocket_lazy",
"libskia",
+ "libperfetto_client_experimental",
],
shared_libs: [
@@ -355,6 +359,7 @@
"server_configurable_flags",
"libimage_io",
"libultrahdr",
+ "libperfetto_c",
],
export_shared_lib_headers: [
// our headers include libnativewindow's public headers
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 17aad43..7a16318 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -220,6 +220,9 @@
extern int register_android_window_WindowInfosListener(JNIEnv* env);
extern int register_android_window_ScreenCapture(JNIEnv* env);
extern int register_jni_common(JNIEnv* env);
+extern int register_android_tracing_PerfettoDataSource(JNIEnv* env);
+extern int register_android_tracing_PerfettoDataSourceInstance(JNIEnv* env);
+extern int register_android_tracing_PerfettoProducer(JNIEnv* env);
// Namespace for Android Runtime flags applied during boot time.
static const char* RUNTIME_NATIVE_BOOT_NAMESPACE = "runtime_native_boot";
@@ -1675,6 +1678,10 @@
REG_JNI(register_android_window_WindowInfosListener),
REG_JNI(register_android_window_ScreenCapture),
REG_JNI(register_jni_common),
+
+ REG_JNI(register_android_tracing_PerfettoDataSource),
+ REG_JNI(register_android_tracing_PerfettoDataSourceInstance),
+ REG_JNI(register_android_tracing_PerfettoProducer),
};
/*
diff --git a/core/jni/android_hardware_OverlayProperties.cpp b/core/jni/android_hardware_OverlayProperties.cpp
index 5b95ee7..f6fe3dd 100644
--- a/core/jni/android_hardware_OverlayProperties.cpp
+++ b/core/jni/android_hardware_OverlayProperties.cpp
@@ -61,13 +61,12 @@
static_cast<int32_t>(HAL_PIXEL_FORMAT_RGBA_FP16)) !=
i.pixelFormats.end() &&
std::find(i.standards.begin(), i.standards.end(),
- static_cast<int32_t>(HAL_DATASPACE_STANDARD_BT2020)) !=
+ static_cast<int32_t>(HAL_DATASPACE_STANDARD_BT709)) !=
i.standards.end() &&
std::find(i.transfers.begin(), i.transfers.end(),
- static_cast<int32_t>(HAL_DATASPACE_TRANSFER_ST2084)) !=
- i.transfers.end() &&
+ static_cast<int32_t>(HAL_DATASPACE_TRANSFER_SRGB)) != i.transfers.end() &&
std::find(i.ranges.begin(), i.ranges.end(),
- static_cast<int32_t>(HAL_DATASPACE_RANGE_FULL)) != i.ranges.end()) {
+ static_cast<int32_t>(HAL_DATASPACE_RANGE_EXTENDED)) != i.ranges.end()) {
return true;
}
}
diff --git a/core/jni/android_tracing_PerfettoDataSource.cpp b/core/jni/android_tracing_PerfettoDataSource.cpp
new file mode 100644
index 0000000..d710698
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSource.cpp
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include "android_tracing_PerfettoDataSource.h"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+static struct {
+ jclass clazz;
+ jmethodID createInstance;
+ jmethodID createTlsState;
+ jmethodID createIncrementalState;
+} gPerfettoDataSourceClassInfo;
+
+static struct {
+ jclass clazz;
+ jmethodID init;
+ jmethodID getAndClearAllPendingTracePackets;
+} gTracingContextClassInfo;
+
+static struct {
+ jclass clazz;
+ jmethodID init;
+} gCreateTlsStateArgsClassInfo;
+
+static struct {
+ jclass clazz;
+ jmethodID init;
+} gCreateIncrementalStateArgsClassInfo;
+
+static JavaVM* gVm;
+
+struct TlsState {
+ jobject jobj;
+};
+
+struct IncrementalState {
+ jobject jobj;
+};
+
+static void traceAllPendingPackets(JNIEnv* env, jobject jCtx, PerfettoDsTracerIterator* ctx) {
+ jobjectArray packets =
+ (jobjectArray)env
+ ->CallObjectMethod(jCtx,
+ gTracingContextClassInfo.getAndClearAllPendingTracePackets);
+ if (env->ExceptionOccurred()) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+
+ LOG_ALWAYS_FATAL("Failed to call java context finalize method");
+ }
+
+ int packets_count = env->GetArrayLength(packets);
+ for (int i = 0; i < packets_count; i++) {
+ jbyteArray packet_proto_buffer = (jbyteArray)env->GetObjectArrayElement(packets, i);
+
+ jbyte* raw_proto_buffer = env->GetByteArrayElements(packet_proto_buffer, 0);
+ int buffer_size = env->GetArrayLength(packet_proto_buffer);
+
+ struct PerfettoDsRootTracePacket trace_packet;
+ PerfettoDsTracerPacketBegin(ctx, &trace_packet);
+ PerfettoPbMsgAppendBytes(&trace_packet.msg.msg, (const uint8_t*)raw_proto_buffer,
+ buffer_size);
+ PerfettoDsTracerPacketEnd(ctx, &trace_packet);
+ }
+}
+
+PerfettoDataSource::PerfettoDataSource(JNIEnv* env, jobject javaDataSource,
+ std::string dataSourceName)
+ : dataSourceName(std::move(dataSourceName)),
+ mJavaDataSource(env->NewGlobalRef(javaDataSource)) {}
+
+jobject PerfettoDataSource::newInstance(JNIEnv* env, void* ds_config, size_t ds_config_size,
+ PerfettoDsInstanceIndex inst_id) {
+ jbyteArray configArray = env->NewByteArray(ds_config_size);
+
+ void* temp = env->GetPrimitiveArrayCritical((jarray)configArray, 0);
+ memcpy(temp, ds_config, ds_config_size);
+ env->ReleasePrimitiveArrayCritical(configArray, temp, 0);
+
+ jobject instance =
+ env->CallObjectMethod(mJavaDataSource, gPerfettoDataSourceClassInfo.createInstance,
+ configArray, inst_id);
+
+ if (env->ExceptionCheck()) {
+ LOGE_EX(env);
+ env->ExceptionClear();
+ LOG_ALWAYS_FATAL("Failed to create new Java Perfetto datasource instance");
+ }
+
+ return instance;
+}
+
+jobject PerfettoDataSource::createTlsStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id) {
+ ScopedLocalRef<jobject> args(env,
+ env->NewObject(gCreateTlsStateArgsClassInfo.clazz,
+ gCreateTlsStateArgsClassInfo.init, mJavaDataSource,
+ inst_id));
+
+ ScopedLocalRef<jobject> tslState(env,
+ env->CallObjectMethod(mJavaDataSource,
+ gPerfettoDataSourceClassInfo
+ .createTlsState,
+ args.get()));
+
+ if (env->ExceptionCheck()) {
+ LOGE_EX(env);
+ env->ExceptionClear();
+ LOG_ALWAYS_FATAL("Failed to create new Java Perfetto incremental state");
+ }
+
+ return env->NewGlobalRef(tslState.get());
+}
+
+jobject PerfettoDataSource::createIncrementalStateGlobalRef(JNIEnv* env,
+ PerfettoDsInstanceIndex inst_id) {
+ ScopedLocalRef<jobject> args(env,
+ env->NewObject(gCreateIncrementalStateArgsClassInfo.clazz,
+ gCreateIncrementalStateArgsClassInfo.init,
+ mJavaDataSource, inst_id));
+
+ ScopedLocalRef<jobject> incrementalState(env,
+ env->CallObjectMethod(mJavaDataSource,
+ gPerfettoDataSourceClassInfo
+ .createIncrementalState,
+ args.get()));
+
+ if (env->ExceptionCheck()) {
+ LOGE_EX(env);
+ env->ExceptionClear();
+ LOG_ALWAYS_FATAL("Failed to create Java Perfetto incremental state");
+ }
+
+ return env->NewGlobalRef(incrementalState.get());
+}
+
+void PerfettoDataSource::trace(JNIEnv* env, jobject traceFunction) {
+ PERFETTO_DS_TRACE(dataSource, ctx) {
+ TlsState* tls_state =
+ reinterpret_cast<TlsState*>(PerfettoDsGetCustomTls(&dataSource, &ctx));
+ IncrementalState* incr_state = reinterpret_cast<IncrementalState*>(
+ PerfettoDsGetIncrementalState(&dataSource, &ctx));
+
+ ScopedLocalRef<jobject> jCtx(env,
+ env->NewObject(gTracingContextClassInfo.clazz,
+ gTracingContextClassInfo.init, &ctx,
+ tls_state->jobj, incr_state->jobj));
+
+ jclass objclass = env->GetObjectClass(traceFunction);
+ jmethodID method =
+ env->GetMethodID(objclass, "trace", "(Landroid/tracing/perfetto/TracingContext;)V");
+ if (method == 0) {
+ LOG_ALWAYS_FATAL("Failed to get method id");
+ }
+
+ env->ExceptionClear();
+
+ env->CallVoidMethod(traceFunction, method, jCtx.get());
+ if (env->ExceptionOccurred()) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ LOG_ALWAYS_FATAL("Failed to call java trace method");
+ }
+
+ traceAllPendingPackets(env, jCtx.get(), &ctx);
+ }
+}
+
+void PerfettoDataSource::flushAll() {
+ PERFETTO_DS_TRACE(dataSource, ctx) {
+ PerfettoDsTracerFlush(&ctx, nullptr, nullptr);
+ }
+}
+
+PerfettoDataSource::~PerfettoDataSource() {
+ JNIEnv* env = AndroidRuntime::getJNIEnv();
+ env->DeleteWeakGlobalRef(mJavaDataSource);
+}
+
+jlong nativeCreate(JNIEnv* env, jclass clazz, jobject javaDataSource, jstring name) {
+ const char* nativeString = env->GetStringUTFChars(name, 0);
+ PerfettoDataSource* dataSource = new PerfettoDataSource(env, javaDataSource, nativeString);
+ env->ReleaseStringUTFChars(name, nativeString);
+
+ dataSource->incStrong((void*)nativeCreate);
+
+ return reinterpret_cast<jlong>(dataSource);
+}
+
+void nativeDestroy(void* ptr) {
+ PerfettoDataSource* dataSource = reinterpret_cast<PerfettoDataSource*>(ptr);
+ dataSource->decStrong((void*)nativeCreate);
+}
+
+static jlong nativeGetFinalizer(JNIEnv* /* env */, jclass /* clazz */) {
+ return static_cast<jlong>(reinterpret_cast<uintptr_t>(&nativeDestroy));
+}
+
+void nativeTrace(JNIEnv* env, jclass clazz, jlong dataSourcePtr, jobject traceFunctionInterface) {
+ sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr);
+
+ datasource->trace(env, traceFunctionInterface);
+}
+
+void nativeFlush(JNIEnv* env, jclass clazz, jobject jCtx, jlong ctxPtr) {
+ auto* ctx = reinterpret_cast<struct PerfettoDsTracerIterator*>(ctxPtr);
+ traceAllPendingPackets(env, jCtx, ctx);
+ PerfettoDsTracerFlush(ctx, nullptr, nullptr);
+}
+
+void nativeFlushAll(JNIEnv* env, jclass clazz, jlong ptr) {
+ sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(ptr);
+ datasource->flushAll();
+}
+
+void nativeRegisterDataSource(JNIEnv* env, jclass clazz, jlong datasource_ptr,
+ int buffer_exhausted_policy) {
+ sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(datasource_ptr);
+
+ struct PerfettoDsParams params = PerfettoDsParamsDefault();
+ params.buffer_exhausted_policy = (PerfettoDsBufferExhaustedPolicy)buffer_exhausted_policy;
+
+ params.user_arg = reinterpret_cast<void*>(datasource.get());
+
+ params.on_setup_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex inst_id,
+ void* ds_config, size_t ds_config_size, void* user_arg,
+ struct PerfettoDsOnSetupArgs*) -> void* {
+ JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+ auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg);
+
+ ScopedLocalRef<jobject> java_data_source_instance(env,
+ datasource->newInstance(env, ds_config,
+ ds_config_size,
+ inst_id));
+
+ auto* datasource_instance =
+ new PerfettoDataSourceInstance(env, java_data_source_instance.get(), inst_id);
+
+ return static_cast<void*>(datasource_instance);
+ };
+
+ params.on_create_tls_cb = [](struct PerfettoDsImpl* ds_impl, PerfettoDsInstanceIndex inst_id,
+ struct PerfettoDsTracerImpl* tracer, void* user_arg) -> void* {
+ JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+ auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg);
+
+ jobject java_tls_state = datasource->createTlsStateGlobalRef(env, inst_id);
+
+ auto* tls_state = new TlsState(java_tls_state);
+
+ return static_cast<void*>(tls_state);
+ };
+
+ params.on_delete_tls_cb = [](void* ptr) {
+ JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+ TlsState* tls_state = reinterpret_cast<TlsState*>(ptr);
+ env->DeleteGlobalRef(tls_state->jobj);
+ delete tls_state;
+ };
+
+ params.on_create_incr_cb = [](struct PerfettoDsImpl* ds_impl, PerfettoDsInstanceIndex inst_id,
+ struct PerfettoDsTracerImpl* tracer, void* user_arg) -> void* {
+ JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+ auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg);
+ jobject java_incr_state = datasource->createIncrementalStateGlobalRef(env, inst_id);
+
+ auto* incr_state = new IncrementalState(java_incr_state);
+ return static_cast<void*>(incr_state);
+ };
+
+ params.on_delete_incr_cb = [](void* ptr) {
+ JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+ IncrementalState* incr_state = reinterpret_cast<IncrementalState*>(ptr);
+ env->DeleteGlobalRef(incr_state->jobj);
+ delete incr_state;
+ };
+
+ params.on_start_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex, void*, void* inst_ctx,
+ struct PerfettoDsOnStartArgs*) {
+ JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+ auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+ datasource_instance->onStart(env);
+ };
+
+ params.on_flush_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex, void*, void* inst_ctx,
+ struct PerfettoDsOnFlushArgs*) {
+ JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+ auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+ datasource_instance->onFlush(env);
+ };
+
+ params.on_stop_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex inst_id, void* user_arg,
+ void* inst_ctx, struct PerfettoDsOnStopArgs*) {
+ JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+ auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+ datasource_instance->onStop(env);
+ };
+
+ params.on_destroy_cb = [](struct PerfettoDsImpl* ds_impl, void* user_arg,
+ void* inst_ctx) -> void {
+ auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+ delete datasource_instance;
+ };
+
+ PerfettoDsRegister(&datasource->dataSource, datasource->dataSourceName.c_str(), params);
+}
+
+jobject nativeGetPerfettoInstanceLocked(JNIEnv* env, jclass clazz, jlong dataSourcePtr,
+ PerfettoDsInstanceIndex instance_idx) {
+ sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr);
+ auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(
+ PerfettoDsImplGetInstanceLocked(datasource->dataSource.impl, instance_idx));
+
+ if (datasource_instance == nullptr) {
+ // datasource instance doesn't exist
+ return nullptr;
+ }
+
+ return datasource_instance->GetJavaDataSourceInstance();
+}
+
+void nativeReleasePerfettoInstanceLocked(JNIEnv* env, jclass clazz, jlong dataSourcePtr,
+ PerfettoDsInstanceIndex instance_idx) {
+ sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr);
+ PerfettoDsImplReleaseInstanceLocked(datasource->dataSource.impl, instance_idx);
+}
+
+const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativeCreate", "(Landroid/tracing/perfetto/DataSource;Ljava/lang/String;)J",
+ (void*)nativeCreate},
+ {"nativeTrace", "(JLandroid/tracing/perfetto/TraceFunction;)V", (void*)nativeTrace},
+ {"nativeFlushAll", "(J)V", (void*)nativeFlushAll},
+ {"nativeGetFinalizer", "()J", (void*)nativeGetFinalizer},
+ {"nativeRegisterDataSource", "(JI)V", (void*)nativeRegisterDataSource},
+ {"nativeGetPerfettoInstanceLocked", "(JI)Landroid/tracing/perfetto/DataSourceInstance;",
+ (void*)nativeGetPerfettoInstanceLocked},
+ {"nativeReleasePerfettoInstanceLocked", "(JI)V",
+ (void*)nativeReleasePerfettoInstanceLocked},
+};
+
+const JNINativeMethod gMethodsTracingContext[] = {
+ /* name, signature, funcPtr */
+ {"nativeFlush", "(Landroid/tracing/perfetto/TracingContext;J)V", (void*)nativeFlush},
+};
+
+int register_android_tracing_PerfettoDataSource(JNIEnv* env) {
+ int res = jniRegisterNativeMethods(env, "android/tracing/perfetto/DataSource", gMethods,
+ NELEM(gMethods));
+
+ LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+ res = jniRegisterNativeMethods(env, "android/tracing/perfetto/TracingContext",
+ gMethodsTracingContext, NELEM(gMethodsTracingContext));
+
+ LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+ if (env->GetJavaVM(&gVm) != JNI_OK) {
+ LOG_ALWAYS_FATAL("Failed to get JavaVM from JNIEnv: %p", env);
+ }
+
+ jclass clazz = env->FindClass("android/tracing/perfetto/DataSource");
+ gPerfettoDataSourceClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+ gPerfettoDataSourceClassInfo.createInstance =
+ env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createInstance",
+ "([BI)Landroid/tracing/perfetto/DataSourceInstance;");
+ gPerfettoDataSourceClassInfo.createTlsState =
+ env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createTlsState",
+ "(Landroid/tracing/perfetto/CreateTlsStateArgs;)Ljava/lang/Object;");
+ gPerfettoDataSourceClassInfo.createIncrementalState =
+ env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createIncrementalState",
+ "(Landroid/tracing/perfetto/CreateIncrementalStateArgs;)Ljava/lang/"
+ "Object;");
+
+ clazz = env->FindClass("android/tracing/perfetto/TracingContext");
+ gTracingContextClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+ gTracingContextClassInfo.init = env->GetMethodID(gTracingContextClassInfo.clazz, "<init>",
+ "(JLjava/lang/Object;Ljava/lang/Object;)V");
+ gTracingContextClassInfo.getAndClearAllPendingTracePackets =
+ env->GetMethodID(gTracingContextClassInfo.clazz, "getAndClearAllPendingTracePackets",
+ "()[[B");
+
+ clazz = env->FindClass("android/tracing/perfetto/CreateTlsStateArgs");
+ gCreateTlsStateArgsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+ gCreateTlsStateArgsClassInfo.init =
+ env->GetMethodID(gCreateTlsStateArgsClassInfo.clazz, "<init>",
+ "(Landroid/tracing/perfetto/DataSource;I)V");
+
+ clazz = env->FindClass("android/tracing/perfetto/CreateIncrementalStateArgs");
+ gCreateIncrementalStateArgsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+ gCreateIncrementalStateArgsClassInfo.init =
+ env->GetMethodID(gCreateIncrementalStateArgsClassInfo.clazz, "<init>",
+ "(Landroid/tracing/perfetto/DataSource;I)V");
+
+ return 0;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoDataSource.h b/core/jni/android_tracing_PerfettoDataSource.h
new file mode 100644
index 0000000..4ddf1d8
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSource.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "android_tracing_PerfettoDataSourceInstance.h"
+#include "core_jni_helpers.h"
+
+namespace android {
+
+class PerfettoDataSource : public virtual RefBase {
+public:
+ const std::string dataSourceName;
+ struct PerfettoDs dataSource = PERFETTO_DS_INIT();
+
+ PerfettoDataSource(JNIEnv* env, jobject java_data_source, std::string data_source_name);
+ ~PerfettoDataSource();
+
+ jobject newInstance(JNIEnv* env, void* ds_config, size_t ds_config_size,
+ PerfettoDsInstanceIndex inst_id);
+
+ jobject createTlsStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id);
+ jobject createIncrementalStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id);
+ void trace(JNIEnv* env, jobject trace_function);
+ void flushAll();
+
+private:
+ jobject mJavaDataSource;
+ std::map<PerfettoDsInstanceIndex, PerfettoDataSourceInstance*> mInstances;
+};
+
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.cpp b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
new file mode 100644
index 0000000..e659bf1
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include "android_tracing_PerfettoDataSourceInstance.h"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+static struct {
+ jclass clazz;
+ jmethodID init;
+} gStartCallbackArgumentsClassInfo;
+
+static struct {
+ jclass clazz;
+ jmethodID init;
+} gFlushCallbackArgumentsClassInfo;
+
+static struct {
+ jclass clazz;
+ jmethodID init;
+} gStopCallbackArgumentsClassInfo;
+
+static JavaVM* gVm;
+
+void callJavaMethodWithArgsObject(JNIEnv* env, jobject classRef, jmethodID method, jobject args) {
+ ScopedLocalRef<jobject> localClassRef(env, env->NewLocalRef(classRef));
+
+ if (localClassRef == nullptr) {
+ ALOGE("Weak reference went out of scope");
+ return;
+ }
+
+ env->CallVoidMethod(localClassRef.get(), method, args);
+
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ LOGE_EX(env);
+ env->ExceptionClear();
+ }
+}
+
+PerfettoDataSourceInstance::PerfettoDataSourceInstance(JNIEnv* env, jobject javaDataSourceInstance,
+ PerfettoDsInstanceIndex inst_idx)
+ : inst_idx(inst_idx), mJavaDataSourceInstance(env->NewGlobalRef(javaDataSourceInstance)) {}
+
+PerfettoDataSourceInstance::~PerfettoDataSourceInstance() {
+ JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+ env->DeleteGlobalRef(mJavaDataSourceInstance);
+}
+
+void PerfettoDataSourceInstance::onStart(JNIEnv* env) {
+ ScopedLocalRef<jobject> args(env,
+ env->NewObject(gStartCallbackArgumentsClassInfo.clazz,
+ gStartCallbackArgumentsClassInfo.init));
+ jclass cls = env->GetObjectClass(mJavaDataSourceInstance);
+ jmethodID mid = env->GetMethodID(cls, "onStart",
+ "(Landroid/tracing/perfetto/StartCallbackArguments;)V");
+
+ callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get());
+}
+
+void PerfettoDataSourceInstance::onFlush(JNIEnv* env) {
+ ScopedLocalRef<jobject> args(env,
+ env->NewObject(gFlushCallbackArgumentsClassInfo.clazz,
+ gFlushCallbackArgumentsClassInfo.init));
+ jclass cls = env->GetObjectClass(mJavaDataSourceInstance);
+ jmethodID mid = env->GetMethodID(cls, "onFlush",
+ "(Landroid/tracing/perfetto/FlushCallbackArguments;)V");
+
+ callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get());
+}
+
+void PerfettoDataSourceInstance::onStop(JNIEnv* env) {
+ ScopedLocalRef<jobject> args(env,
+ env->NewObject(gStopCallbackArgumentsClassInfo.clazz,
+ gStopCallbackArgumentsClassInfo.init));
+ jclass cls = env->GetObjectClass(mJavaDataSourceInstance);
+ jmethodID mid =
+ env->GetMethodID(cls, "onStop", "(Landroid/tracing/perfetto/StopCallbackArguments;)V");
+
+ callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get());
+}
+
+int register_android_tracing_PerfettoDataSourceInstance(JNIEnv* env) {
+ if (env->GetJavaVM(&gVm) != JNI_OK) {
+ LOG_ALWAYS_FATAL("Failed to get JavaVM from JNIEnv: %p", env);
+ }
+
+ jclass clazz = env->FindClass("android/tracing/perfetto/StartCallbackArguments");
+ gStartCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+ gStartCallbackArgumentsClassInfo.init =
+ env->GetMethodID(gStartCallbackArgumentsClassInfo.clazz, "<init>", "()V");
+
+ clazz = env->FindClass("android/tracing/perfetto/FlushCallbackArguments");
+ gFlushCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+ gFlushCallbackArgumentsClassInfo.init =
+ env->GetMethodID(gFlushCallbackArgumentsClassInfo.clazz, "<init>", "()V");
+
+ clazz = env->FindClass("android/tracing/perfetto/StopCallbackArguments");
+ gStopCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+ gStopCallbackArgumentsClassInfo.init =
+ env->GetMethodID(gStopCallbackArgumentsClassInfo.clazz, "<init>", "()V");
+
+ return 0;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.h b/core/jni/android_tracing_PerfettoDataSourceInstance.h
new file mode 100644
index 0000000..d577655
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSourceInstance.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+class PerfettoDataSourceInstance {
+public:
+ PerfettoDataSourceInstance(JNIEnv* env, jobject javaDataSourceInstance,
+ PerfettoDsInstanceIndex inst_idx);
+ ~PerfettoDataSourceInstance();
+
+ void onStart(JNIEnv* env);
+ void onFlush(JNIEnv* env);
+ void onStop(JNIEnv* env);
+
+ jobject GetJavaDataSourceInstance() {
+ return mJavaDataSourceInstance;
+ }
+
+ PerfettoDsInstanceIndex getIndex() {
+ return inst_idx;
+ }
+
+private:
+ PerfettoDsInstanceIndex inst_idx;
+ jobject mJavaDataSourceInstance;
+};
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoProducer.cpp b/core/jni/android_tracing_PerfettoProducer.cpp
new file mode 100644
index 0000000..ce72f58
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoProducer.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "android_tracing_PerfettoDataSource.h"
+#include "core_jni_helpers.h"
+
+namespace android {
+
+void perfettoProducerInit(JNIEnv* env, jclass clazz, int backends) {
+ struct PerfettoProducerInitArgs args = PERFETTO_PRODUCER_INIT_ARGS_INIT();
+ args.backends = (PerfettoBackendTypes)backends;
+ PerfettoProducerInit(args);
+}
+
+const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativePerfettoProducerInit", "(I)V", (void*)perfettoProducerInit},
+};
+
+int register_android_tracing_PerfettoProducer(JNIEnv* env) {
+ int res = jniRegisterNativeMethods(env, "android/tracing/perfetto/Producer", gMethods,
+ NELEM(gMethods));
+
+ LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+ return 0;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/proto/android/service/notification.proto b/core/proto/android/service/notification.proto
index a2978be..ad36b1c 100644
--- a/core/proto/android/service/notification.proto
+++ b/core/proto/android/service/notification.proto
@@ -360,7 +360,7 @@
optional ConversationType allow_conversations_from = 19;
- optional ChannelType allow_channels = 20;
+ optional ChannelPolicy allow_channels = 20;
}
// Enum identifying the type of rule that changed; values set to match ones used in the
@@ -373,8 +373,8 @@
// Enum used in DNDPolicyProto to indicate the type of channels permitted to
// break through DND. Mirrors values in ZenPolicy.
-enum ChannelType {
- CHANNEL_TYPE_UNSET = 0;
- CHANNEL_TYPE_PRIORITY = 1;
- CHANNEL_TYPE_NONE = 2;
+enum ChannelPolicy {
+ CHANNEL_POLICY_UNSET = 0;
+ CHANNEL_POLICY_PRIORITY = 1;
+ CHANNEL_POLICY_NONE = 2;
}
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 6019524..8fae6db 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -506,12 +506,6 @@
receivers, and providers; it can not be used with activities. -->
<attr name="singleUser" format="boolean" />
- <!-- If set to true, only a single instance of this component will
- run and be available for the SYSTEM user. Non SYSTEM users will not be
- allowed to access the component if this flag is enabled.
- This flag can be used with services, receivers, providers and activities. -->
- <attr name="systemUserOnly" format="boolean" />
-
<!-- Specify a specific process that the associated code is to run in.
Use with the application tag (to supply a default process for all
application components), or with the activity, receiver, service,
@@ -2865,7 +2859,6 @@
Context.createAttributionContext() using the first attribution tag
contained here. -->
<attr name="attributionTags" />
- <attr name="systemUserOnly" format="boolean" />
</declare-styleable>
<!-- Attributes that can be supplied in an AndroidManifest.xml
@@ -3024,7 +3017,6 @@
ignored when the process is bound into a shared isolated process by a client.
-->
<attr name="allowSharedIsolatedProcess" format="boolean" />
- <attr name="systemUserOnly" format="boolean" />
</declare-styleable>
<!-- @hide The <code>apex-system-service</code> tag declares an apex system service
@@ -3152,7 +3144,7 @@
<attr name="uiOptions" />
<attr name="parentActivityName" />
<attr name="singleUser" />
- <!-- This broadcast receiver or activity will only receive broadcasts for the
+ <!-- @hide This broadcast receiver or activity will only receive broadcasts for the
system user-->
<attr name="systemUserOnly" format="boolean" />
<attr name="persistableMode" />
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 7b5c49c..53b473e 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -119,8 +119,6 @@
<public name="optional"/>
<!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") -->
<public name="adServiceTypes" />
- <!-- @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") -->
- <public name="systemUserOnly"/>
</staging-public-group>
<staging-public-group type="id" first-id="0x01bc0000">
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index c058174..e18de2e 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -30,6 +30,8 @@
android_test {
name: "FrameworksCoreTests",
+ // FrameworksCoreTestsRavenwood references the .aapt.srcjar
+ use_resource_processor: false,
srcs: [
"src/**/*.java",
@@ -83,6 +85,10 @@
"com.android.text.flags-aconfig-java",
"flag-junit",
"ravenwood-junit",
+ "perfetto_trace_java_protos",
+ "flickerlib-parsers",
+ "flickerlib-trace_processor_shell",
+ "mockito-target-extended-minus-junit4",
],
libs: [
diff --git a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
index 9d85b65..1925588 100644
--- a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
+++ b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java
@@ -16,8 +16,6 @@
package android.app;
-import static com.google.common.truth.Truth.assertThat;
-
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.fail;
@@ -28,8 +26,6 @@
import android.os.Parcel;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
-import android.service.notification.ZenDeviceEffects;
-import android.service.notification.ZenPolicy;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -230,66 +226,4 @@
assertThrows(IllegalArgumentException.class, () -> builder.setType(100));
}
-
- @Test
- @EnableFlags(Flags.FLAG_MODES_API)
- public void testCanUpdate_nullPolicyAndDeviceEffects() {
- AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name",
- Uri.parse("uri://short"));
-
- AutomaticZenRule rule = builder.setUserModifiedFields(0)
- .setZenPolicy(null)
- .setDeviceEffects(null)
- .build();
-
- assertThat(rule.canUpdate()).isTrue();
-
- rule = builder.setUserModifiedFields(1).build();
- assertThat(rule.canUpdate()).isFalse();
- }
-
- @Test
- @EnableFlags(Flags.FLAG_MODES_API)
- public void testCanUpdate_policyModified() {
- ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder().setUserModifiedFields(0);
- ZenPolicy policy = policyBuilder.build();
-
- AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name",
- Uri.parse("uri://short"));
- AutomaticZenRule rule = builder.setUserModifiedFields(0)
- .setZenPolicy(policy)
- .setDeviceEffects(null).build();
-
- // Newly created ZenPolicy is not user modified.
- assertThat(policy.getUserModifiedFields()).isEqualTo(0);
- assertThat(rule.canUpdate()).isTrue();
-
- policy = policyBuilder.setUserModifiedFields(1).build();
- assertThat(policy.getUserModifiedFields()).isEqualTo(1);
- rule = builder.setZenPolicy(policy).build();
- assertThat(rule.canUpdate()).isFalse();
- }
-
- @Test
- @EnableFlags(Flags.FLAG_MODES_API)
- public void testCanUpdate_deviceEffectsModified() {
- ZenDeviceEffects.Builder deviceEffectsBuilder =
- new ZenDeviceEffects.Builder().setUserModifiedFields(0);
- ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build();
-
- AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name",
- Uri.parse("uri://short"));
- AutomaticZenRule rule = builder.setUserModifiedFields(0)
- .setZenPolicy(null)
- .setDeviceEffects(deviceEffects).build();
-
- // Newly created ZenDeviceEffects is not user modified.
- assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(0);
- assertThat(rule.canUpdate()).isTrue();
-
- deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build();
- assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1);
- rule = builder.setDeviceEffects(deviceEffects).build();
- assertThat(rule.canUpdate()).isFalse();
- }
}
diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
new file mode 100644
index 0000000..bd2f36f
--- /dev/null
+++ b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
@@ -0,0 +1,664 @@
+/*
+ * Copyright (C) 2023 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.tracing.perfetto;
+
+import static android.internal.perfetto.protos.PerfettoTrace.TestEvent.PAYLOAD;
+import static android.internal.perfetto.protos.PerfettoTrace.TestEvent.TestPayload.SINGLE_INT;
+import static android.internal.perfetto.protos.PerfettoTrace.TracePacket.FOR_TESTING;
+
+import static java.io.File.createTempFile;
+import static java.nio.file.Files.createTempDirectory;
+
+import android.internal.perfetto.protos.PerfettoTrace;
+import android.tools.common.ScenarioBuilder;
+import android.tools.common.Tag;
+import android.tools.common.io.TraceType;
+import android.tools.device.traces.TraceConfig;
+import android.tools.device.traces.TraceConfigs;
+import android.tools.device.traces.io.ResultReader;
+import android.tools.device.traces.io.ResultWriter;
+import android.tools.device.traces.monitors.PerfettoTraceMonitor;
+import android.tools.device.traces.monitors.TraceMonitor;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.truth.Truth;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import perfetto.protos.PerfettoConfig;
+import perfetto.protos.TracePacketOuterClass;
+
+@RunWith(AndroidJUnit4.class)
+public class DataSourceTest {
+ private final File mTracingDirectory = createTempDirectory("temp").toFile();
+
+ private final ResultWriter mWriter = new ResultWriter()
+ .forScenario(new ScenarioBuilder()
+ .forClass(createTempFile("temp", "").getName()).build())
+ .withOutputDir(mTracingDirectory)
+ .setRunComplete();
+
+ private final TraceConfigs mTraceConfig = new TraceConfigs(
+ new TraceConfig(false, true, false),
+ new TraceConfig(false, true, false),
+ new TraceConfig(false, true, false),
+ new TraceConfig(false, true, false)
+ );
+
+ private static TestDataSource sTestDataSource;
+
+ private static TestDataSource.DataSourceInstanceProvider sInstanceProvider;
+ private static TestDataSource.TlsStateProvider sTlsStateProvider;
+ private static TestDataSource.IncrementalStateProvider sIncrementalStateProvider;
+
+ public DataSourceTest() throws IOException {}
+
+ @BeforeClass
+ public static void beforeAll() {
+ Producer.init(InitArguments.DEFAULTS);
+ setupProviders();
+ sTestDataSource = new TestDataSource(
+ (ds, idx, configStream) -> sInstanceProvider.provide(ds, idx, configStream),
+ args -> sTlsStateProvider.provide(args),
+ args -> sIncrementalStateProvider.provide(args));
+ sTestDataSource.register(DataSourceParams.DEFAULTS);
+ }
+
+ private static void setupProviders() {
+ sInstanceProvider = (ds, idx, configStream) ->
+ new TestDataSource.TestDataSourceInstance(ds, idx);
+ sTlsStateProvider = args -> new TestDataSource.TestTlsState();
+ sIncrementalStateProvider = args -> new TestDataSource.TestIncrementalState();
+ }
+
+ @Before
+ public void setup() {
+ setupProviders();
+ }
+
+ @Test
+ public void canTraceData() throws InvalidProtocolBufferException {
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+
+ try {
+ traceMonitor.start();
+
+ sTestDataSource.trace((ctx) -> {
+ final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+ long forTestingToken = protoOutputStream.start(FOR_TESTING);
+ long payloadToken = protoOutputStream.start(PAYLOAD);
+ protoOutputStream.write(SINGLE_INT, 10);
+ protoOutputStream.end(payloadToken);
+ protoOutputStream.end(forTestingToken);
+ });
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+ assert rawProtoFromFile != null;
+ final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace
+ .parseFrom(rawProtoFromFile);
+
+ Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+ final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+ .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+ final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream()
+ .filter(it -> it.getForTesting().getPayload().getSingleInt() == 10).toList();
+ Truth.assertThat(matchingPackets).hasSize(1);
+ }
+
+ @Test
+ public void canUseTlsStateForCustomState() {
+ final int expectedStateTestValue = 10;
+ final AtomicInteger actualStateTestValue = new AtomicInteger();
+
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+
+ try {
+ traceMonitor.start();
+
+ sTestDataSource.trace((ctx) -> {
+ TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+ state.testStateValue = expectedStateTestValue;
+ });
+
+ sTestDataSource.trace((ctx) -> {
+ TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+ actualStateTestValue.set(state.testStateValue);
+ });
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ Truth.assertThat(actualStateTestValue.get()).isEqualTo(expectedStateTestValue);
+ }
+
+ @Test
+ public void eachInstanceHasOwnTlsState() {
+ final int[] expectedStateTestValues = new int[] { 1, 2 };
+ final int[] actualStateTestValues = new int[] { 0, 0 };
+
+ final TraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+ final TraceMonitor traceMonitor2 = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+
+ try {
+ traceMonitor1.start();
+ try {
+ traceMonitor2.start();
+
+ AtomicInteger index = new AtomicInteger(0);
+ sTestDataSource.trace((ctx) -> {
+ TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+ state.testStateValue = expectedStateTestValues[index.getAndIncrement()];
+ });
+
+ index.set(0);
+ sTestDataSource.trace((ctx) -> {
+ TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+ actualStateTestValues[index.getAndIncrement()] = state.testStateValue;
+ });
+ } finally {
+ traceMonitor1.stop(mWriter);
+ }
+ } finally {
+ traceMonitor2.stop(mWriter);
+ }
+
+ Truth.assertThat(actualStateTestValues[0]).isEqualTo(expectedStateTestValues[0]);
+ Truth.assertThat(actualStateTestValues[1]).isEqualTo(expectedStateTestValues[1]);
+ }
+
+ @Test
+ public void eachThreadHasOwnTlsState() throws InterruptedException {
+ final int thread1ExpectedStateValue = 1;
+ final int thread2ExpectedStateValue = 2;
+
+ final AtomicInteger thread1ActualStateValue = new AtomicInteger();
+ final AtomicInteger thread2ActualStateValue = new AtomicInteger();
+
+ final CountDownLatch setUpLatch = new CountDownLatch(2);
+ final CountDownLatch setStateLatch = new CountDownLatch(2);
+ final CountDownLatch setOutStateLatch = new CountDownLatch(2);
+
+ final RunnableCreator createTask = (stateValue, stateOut) -> () -> {
+ Producer.init(InitArguments.DEFAULTS);
+
+ setUpLatch.countDown();
+
+ try {
+ setUpLatch.await(3, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ sTestDataSource.trace((ctx) -> {
+ TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+ state.testStateValue = stateValue;
+ setStateLatch.countDown();
+ });
+
+ try {
+ setStateLatch.await(3, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ sTestDataSource.trace((ctx) -> {
+ stateOut.set(ctx.getCustomTlsState().testStateValue);
+ setOutStateLatch.countDown();
+ });
+ };
+
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+
+ try {
+ traceMonitor.start();
+
+ new Thread(
+ createTask.create(thread1ExpectedStateValue, thread1ActualStateValue)).start();
+ new Thread(
+ createTask.create(thread2ExpectedStateValue, thread2ActualStateValue)).start();
+
+ setOutStateLatch.await(3, TimeUnit.SECONDS);
+
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ Truth.assertThat(thread1ActualStateValue.get()).isEqualTo(thread1ExpectedStateValue);
+ Truth.assertThat(thread2ActualStateValue.get()).isEqualTo(thread2ExpectedStateValue);
+ }
+
+ @Test
+ public void incrementalStateIsReset() throws InterruptedException {
+
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build())
+ .setIncrementalTimeout(10)
+ .build();
+
+ final AtomicInteger testStateValue = new AtomicInteger();
+ try {
+ traceMonitor.start();
+
+ sTestDataSource.trace(ctx -> ctx.getIncrementalState().testStateValue = 1);
+
+ // Timeout to make sure the incremental state is cleared.
+ Thread.sleep(1000);
+
+ sTestDataSource.trace(ctx ->
+ testStateValue.set(ctx.getIncrementalState().testStateValue));
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ Truth.assertThat(testStateValue.get()).isNotEqualTo(1);
+ }
+
+ @Test
+ public void getInstanceConfigOnCreateInstance() throws IOException {
+ final int expectedDummyIntValue = 10;
+ AtomicReference<ProtoInputStream> configStream = new AtomicReference<>();
+ sInstanceProvider = (ds, idx, config) -> {
+ configStream.set(config);
+ return new TestDataSource.TestDataSourceInstance(ds, idx);
+ };
+
+ final TraceMonitor monitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name)
+ .setForTesting(PerfettoConfig.TestConfig.newBuilder().setDummyFields(
+ PerfettoConfig.TestConfig.DummyFields.newBuilder()
+ .setFieldInt32(expectedDummyIntValue)
+ .build())
+ .build())
+ .build())
+ .build();
+
+ try {
+ monitor.start();
+ } finally {
+ monitor.stop(mWriter);
+ }
+
+ int configDummyIntValue = 0;
+ while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (configStream.get().getFieldNumber()
+ == (int) PerfettoTrace.DataSourceConfig.FOR_TESTING) {
+ final long forTestingToken = configStream.get()
+ .start(PerfettoTrace.DataSourceConfig.FOR_TESTING);
+ while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (configStream.get().getFieldNumber()
+ == (int) PerfettoTrace.TestConfig.DUMMY_FIELDS) {
+ final long dummyFieldsToken = configStream.get()
+ .start(PerfettoTrace.TestConfig.DUMMY_FIELDS);
+ while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (configStream.get().getFieldNumber()
+ == (int) PerfettoTrace.TestConfig.DummyFields.FIELD_INT32) {
+ int val = configStream.get().readInt(
+ PerfettoTrace.TestConfig.DummyFields.FIELD_INT32);
+ if (val != 0) {
+ configDummyIntValue = val;
+ break;
+ }
+ }
+ }
+ configStream.get().end(dummyFieldsToken);
+ break;
+ }
+ }
+ configStream.get().end(forTestingToken);
+ break;
+ }
+ }
+
+ Truth.assertThat(configDummyIntValue).isEqualTo(expectedDummyIntValue);
+ }
+
+ @Test
+ public void multipleTraceInstances() throws IOException, InterruptedException {
+ final int instanceCount = 3;
+
+ final List<TraceMonitor> monitors = new ArrayList<>();
+ final List<ResultWriter> writers = new ArrayList<>();
+
+ for (int i = 0; i < instanceCount; i++) {
+ final ResultWriter writer = new ResultWriter()
+ .forScenario(new ScenarioBuilder()
+ .forClass(createTempFile("temp", "").getName()).build())
+ .withOutputDir(mTracingDirectory)
+ .setRunComplete();
+ writers.add(writer);
+ }
+
+ // Start at 1 because 0 is considered null value so payload will be ignored in that case
+ TestDataSource.TestTlsState.lastIndex = 1;
+
+ final AtomicInteger traceCallCount = new AtomicInteger();
+ final CountDownLatch latch = new CountDownLatch(instanceCount);
+
+ try {
+ // Start instances
+ for (int i = 0; i < instanceCount; i++) {
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+ monitors.add(traceMonitor);
+ traceMonitor.start();
+ }
+
+ // Trace the stateIndex of the tracing instance.
+ sTestDataSource.trace(ctx -> {
+ final int testIntValue = ctx.getCustomTlsState().stateIndex;
+ traceCallCount.incrementAndGet();
+
+ final ProtoOutputStream os = ctx.newTracePacket();
+ long forTestingToken = os.start(FOR_TESTING);
+ long payloadToken = os.start(PAYLOAD);
+ os.write(SINGLE_INT, testIntValue);
+ os.end(payloadToken);
+ os.end(forTestingToken);
+
+ latch.countDown();
+ });
+ } finally {
+ // Stop instances
+ for (int i = 0; i < instanceCount; i++) {
+ final TraceMonitor monitor = monitors.get(i);
+ final ResultWriter writer = writers.get(i);
+ monitor.stop(writer);
+ }
+ }
+
+ latch.await(3, TimeUnit.SECONDS);
+ Truth.assertThat(traceCallCount.get()).isEqualTo(instanceCount);
+
+ for (int i = 0; i < instanceCount; i++) {
+ final int expectedTracedValue = i + 1;
+ final ResultWriter writer = writers.get(i);
+ final ResultReader reader = new ResultReader(writer.write(), mTraceConfig);
+ final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+ assert rawProtoFromFile != null;
+ final perfetto.protos.TraceOuterClass.Trace trace =
+ perfetto.protos.TraceOuterClass.Trace.parseFrom(rawProtoFromFile);
+
+ Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+ final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+ .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+ Truth.assertWithMessage("One packet has for testing data")
+ .that(tracePackets).hasSize(1);
+
+ final List<TracePacketOuterClass.TracePacket> matchingPackets =
+ tracePackets.stream()
+ .filter(it -> it.getForTesting().getPayload()
+ .getSingleInt() == expectedTracedValue).toList();
+ Truth.assertWithMessage(
+ "One packet has testing data with a payload with the expected value")
+ .that(matchingPackets).hasSize(1);
+ }
+ }
+
+ @Test
+ public void onStartCallbackTriggered() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ final AtomicBoolean callbackCalled = new AtomicBoolean(false);
+ sInstanceProvider = (ds, idx, config) -> new TestDataSource.TestDataSourceInstance(
+ ds,
+ idx,
+ (args) -> {
+ callbackCalled.set(true);
+ latch.countDown();
+ },
+ (args) -> {},
+ (args) -> {}
+ );
+
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+
+ Truth.assertThat(callbackCalled.get()).isFalse();
+ try {
+ traceMonitor.start();
+ latch.await(3, TimeUnit.SECONDS);
+ Truth.assertThat(callbackCalled.get()).isTrue();
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+ }
+
+ @Test
+ public void onFlushCallbackTriggered() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicBoolean callbackCalled = new AtomicBoolean(false);
+ sInstanceProvider = (ds, idx, config) ->
+ new TestDataSource.TestDataSourceInstance(
+ ds,
+ idx,
+ (args) -> {},
+ (args) -> {
+ callbackCalled.set(true);
+ latch.countDown();
+ },
+ (args) -> {}
+ );
+
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+
+ try {
+ traceMonitor.start();
+ Truth.assertThat(callbackCalled.get()).isFalse();
+ sTestDataSource.trace((ctx) -> {
+ final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+ long forTestingToken = protoOutputStream.start(FOR_TESTING);
+ long payloadToken = protoOutputStream.start(PAYLOAD);
+ protoOutputStream.write(SINGLE_INT, 10);
+ protoOutputStream.end(payloadToken);
+ protoOutputStream.end(forTestingToken);
+ });
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ latch.await(3, TimeUnit.SECONDS);
+ Truth.assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ public void onStopCallbackTriggered() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicBoolean callbackCalled = new AtomicBoolean(false);
+ sInstanceProvider = (ds, idx, config) ->
+ new TestDataSource.TestDataSourceInstance(
+ ds,
+ idx,
+ (args) -> {},
+ (args) -> {},
+ (args) -> {
+ callbackCalled.set(true);
+ latch.countDown();
+ }
+ );
+
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+
+ try {
+ traceMonitor.start();
+ Truth.assertThat(callbackCalled.get()).isFalse();
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ latch.await(3, TimeUnit.SECONDS);
+ Truth.assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ public void canUseDataSourceInstanceToCreateTlsState() throws InvalidProtocolBufferException {
+ final Object testObject = new Object();
+
+ sInstanceProvider = (ds, idx, configStream) -> {
+ final TestDataSource.TestDataSourceInstance dsInstance =
+ new TestDataSource.TestDataSourceInstance(ds, idx);
+ dsInstance.testObject = testObject;
+ return dsInstance;
+ };
+
+ sTlsStateProvider = args -> {
+ final TestDataSource.TestTlsState tlsState = new TestDataSource.TestTlsState();
+
+ try (TestDataSource.TestDataSourceInstance dataSourceInstance =
+ args.getDataSourceInstanceLocked()) {
+ if (dataSourceInstance != null) {
+ tlsState.testStateValue = dataSourceInstance.testObject.hashCode();
+ }
+ }
+
+ return tlsState;
+ };
+
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+
+ try {
+ traceMonitor.start();
+ sTestDataSource.trace((ctx) -> {
+ final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+ long forTestingToken = protoOutputStream.start(FOR_TESTING);
+ long payloadToken = protoOutputStream.start(PAYLOAD);
+ protoOutputStream.write(SINGLE_INT, ctx.getCustomTlsState().testStateValue);
+ protoOutputStream.end(payloadToken);
+ protoOutputStream.end(forTestingToken);
+ });
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+ assert rawProtoFromFile != null;
+ final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace
+ .parseFrom(rawProtoFromFile);
+
+ Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+ final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+ .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+ final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream()
+ .filter(it -> it.getForTesting().getPayload().getSingleInt()
+ == testObject.hashCode()).toList();
+ Truth.assertThat(matchingPackets).hasSize(1);
+ }
+
+ @Test
+ public void canUseDataSourceInstanceToCreateIncrementalState()
+ throws InvalidProtocolBufferException {
+ final Object testObject = new Object();
+
+ sInstanceProvider = (ds, idx, configStream) -> {
+ final TestDataSource.TestDataSourceInstance dsInstance =
+ new TestDataSource.TestDataSourceInstance(ds, idx);
+ dsInstance.testObject = testObject;
+ return dsInstance;
+ };
+
+ sIncrementalStateProvider = args -> {
+ final TestDataSource.TestIncrementalState incrementalState =
+ new TestDataSource.TestIncrementalState();
+
+ try (TestDataSource.TestDataSourceInstance dataSourceInstance =
+ args.getDataSourceInstanceLocked()) {
+ if (dataSourceInstance != null) {
+ incrementalState.testStateValue = dataSourceInstance.testObject.hashCode();
+ }
+ }
+
+ return incrementalState;
+ };
+
+ final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+ .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+ .setName(sTestDataSource.name).build()).build();
+
+ try {
+ traceMonitor.start();
+ sTestDataSource.trace((ctx) -> {
+ final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+ long forTestingToken = protoOutputStream.start(FOR_TESTING);
+ long payloadToken = protoOutputStream.start(PAYLOAD);
+ protoOutputStream.write(SINGLE_INT, ctx.getIncrementalState().testStateValue);
+ protoOutputStream.end(payloadToken);
+ protoOutputStream.end(forTestingToken);
+ });
+ } finally {
+ traceMonitor.stop(mWriter);
+ }
+
+ final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+ final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+ assert rawProtoFromFile != null;
+ final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace
+ .parseFrom(rawProtoFromFile);
+
+ Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+ final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+ .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+ final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream()
+ .filter(it -> it.getForTesting().getPayload().getSingleInt()
+ == testObject.hashCode()).toList();
+ Truth.assertThat(matchingPackets).hasSize(1);
+ }
+
+ interface RunnableCreator {
+ Runnable create(int state, AtomicInteger stateOut);
+ }
+}
diff --git a/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java b/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java
new file mode 100644
index 0000000..d78f78b
--- /dev/null
+++ b/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2023 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.tracing.perfetto;
+
+import android.util.proto.ProtoInputStream;
+
+import java.util.UUID;
+import java.util.function.Consumer;
+
+public class TestDataSource extends DataSource<TestDataSource.TestDataSourceInstance,
+ TestDataSource.TestTlsState, TestDataSource.TestIncrementalState> {
+ private final DataSourceInstanceProvider mDataSourceInstanceProvider;
+ private final TlsStateProvider mTlsStateProvider;
+ private final IncrementalStateProvider mIncrementalStateProvider;
+
+ interface DataSourceInstanceProvider {
+ TestDataSourceInstance provide(
+ TestDataSource dataSource, int instanceIndex, ProtoInputStream configStream);
+ }
+
+ interface TlsStateProvider {
+ TestTlsState provide(CreateTlsStateArgs<TestDataSourceInstance> args);
+ }
+
+ interface IncrementalStateProvider {
+ TestIncrementalState provide(CreateIncrementalStateArgs<TestDataSourceInstance> args);
+ }
+
+ public TestDataSource() {
+ this((ds, idx, config) -> new TestDataSourceInstance(ds, idx),
+ args -> new TestTlsState(), args -> new TestIncrementalState());
+ }
+
+ public TestDataSource(
+ DataSourceInstanceProvider dataSourceInstanceProvider,
+ TlsStateProvider tlsStateProvider,
+ IncrementalStateProvider incrementalStateProvider
+ ) {
+ super("android.tracing.perfetto.TestDataSource#" + UUID.randomUUID().toString());
+ this.mDataSourceInstanceProvider = dataSourceInstanceProvider;
+ this.mTlsStateProvider = tlsStateProvider;
+ this.mIncrementalStateProvider = incrementalStateProvider;
+ }
+
+ @Override
+ public TestDataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) {
+ return mDataSourceInstanceProvider.provide(this, instanceIndex, configStream);
+ }
+
+ @Override
+ public TestTlsState createTlsState(CreateTlsStateArgs args) {
+ return mTlsStateProvider.provide(args);
+ }
+
+ @Override
+ public TestIncrementalState createIncrementalState(CreateIncrementalStateArgs args) {
+ return mIncrementalStateProvider.provide(args);
+ }
+
+ public static class TestTlsState {
+ public int testStateValue;
+ public int stateIndex = lastIndex++;
+
+ public static int lastIndex = 0;
+ }
+
+ public static class TestIncrementalState {
+ public int testStateValue;
+ }
+
+ public static class TestDataSourceInstance extends DataSourceInstance {
+ public Object testObject;
+ Consumer<StartCallbackArguments> mStartCallback;
+ Consumer<FlushCallbackArguments> mFlushCallback;
+ Consumer<StopCallbackArguments> mStopCallback;
+
+ public TestDataSourceInstance(DataSource dataSource, int instanceIndex) {
+ this(dataSource, instanceIndex, args -> {}, args -> {}, args -> {});
+ }
+
+ public TestDataSourceInstance(
+ DataSource dataSource,
+ int instanceIndex,
+ Consumer<StartCallbackArguments> startCallback,
+ Consumer<FlushCallbackArguments> flushCallback,
+ Consumer<StopCallbackArguments> stopCallback) {
+ super(dataSource, instanceIndex);
+ this.mStartCallback = startCallback;
+ this.mFlushCallback = flushCallback;
+ this.mStopCallback = stopCallback;
+ }
+
+ @Override
+ public void onStart(StartCallbackArguments args) {
+ this.mStartCallback.accept(args);
+ }
+
+ @Override
+ public void onFlush(FlushCallbackArguments args) {
+ this.mFlushCallback.accept(args);
+ }
+
+ @Override
+ public void onStop(StopCallbackArguments args) {
+ this.mStopCallback.accept(args);
+ }
+ }
+}
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 917a300..3a778c3 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -535,6 +535,12 @@
"group": "WM_DEBUG_TASKS",
"at": "com\/android\/server\/wm\/ActivityStarter.java"
},
+ "-1583619037": {
+ "message": "Failed to register MediaProjectionWatcherCallback",
+ "level": "ERROR",
+ "group": "WM_ERROR",
+ "at": "com\/android\/server\/wm\/ScreenRecordingCallbackController.java"
+ },
"-1582845629": {
"message": "Starting animation on %s",
"level": "VERBOSE",
@@ -2983,12 +2989,6 @@
"group": "WM_DEBUG_SCREEN_ON",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
- "466506262": {
- "message": "Clear freezing of %s: visible=%b freezing=%b",
- "level": "VERBOSE",
- "group": "WM_DEBUG_ORIENTATION",
- "at": "com\/android\/server\/wm\/ActivityRecord.java"
- },
"485170982": {
"message": "Not finishing noHistory %s on stop because we're just sleeping",
"level": "DEBUG",
diff --git a/framework-jarjar-rules.txt b/framework-jarjar-rules.txt
index 03b268d..6339a87 100644
--- a/framework-jarjar-rules.txt
+++ b/framework-jarjar-rules.txt
@@ -8,3 +8,6 @@
# for modules-utils-build dependency
rule com.android.modules.utils.build.** android.internal.modules.utils.build.@1
+
+# For Perfetto proto dependencies
+rule perfetto.protos.** android.internal.perfetto.protos.@1
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index c5a2f98..f6ba103 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -65,8 +65,6 @@
private long mNativeShader;
private long mNativeColorFilter;
- private static boolean sIsRobolectric = Build.FINGERPRINT.equals("robolectric");
-
// Use a Holder to allow static initialization of Paint in the boot image.
private static class NoImagePreloadHolder {
public static final NativeAllocationRegistry sRegistry =
@@ -3393,13 +3391,8 @@
return 0.0f;
}
- if (sIsRobolectric) {
- return nGetRunCharacterAdvance(mNativePaint, text, start, end,
- contextStart, contextEnd, isRtl, offset, advances, advancesIndex, drawBounds);
- } else {
- return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
- isRtl, offset, advances, advancesIndex, drawBounds, runInfo);
- }
+ return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
+ isRtl, offset, advances, advancesIndex, drawBounds, runInfo);
}
/**
diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java
index ddae673..b21bf11 100644
--- a/graphics/java/android/graphics/text/LineBreakConfig.java
+++ b/graphics/java/android/graphics/text/LineBreakConfig.java
@@ -23,9 +23,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.compat.CompatChanges;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledSince;
+import android.app.ActivityThread;
import android.os.Build;
import android.os.LocaleList;
import android.os.Parcel;
@@ -43,15 +41,6 @@
* line-break property</a> for more information.
*/
public final class LineBreakConfig implements Parcelable {
-
- /**
- * A feature ID for automatic line break word style.
- * @hide
- */
- @ChangeId
- @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
- public static final long WORD_STYLE_AUTO = 280005585L;
-
/**
* No hyphenation preference is specified.
*
@@ -487,8 +476,15 @@
* @hide
*/
public static @LineBreakStyle int getResolvedLineBreakStyle(@Nullable LineBreakConfig config) {
- final int defaultStyle = CompatChanges.isChangeEnabled(WORD_STYLE_AUTO)
- ? LINE_BREAK_STYLE_AUTO : LINE_BREAK_STYLE_NONE;
+ final int targetSdkVersion = ActivityThread.currentApplication().getApplicationInfo()
+ .targetSdkVersion;
+ final int defaultStyle;
+ final int vicVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM;
+ if (targetSdkVersion >= vicVersion) {
+ defaultStyle = LINE_BREAK_STYLE_AUTO;
+ } else {
+ defaultStyle = LINE_BREAK_STYLE_NONE;
+ }
if (config == null) {
return defaultStyle;
}
@@ -515,8 +511,15 @@
*/
public static @LineBreakWordStyle int getResolvedLineBreakWordStyle(
@Nullable LineBreakConfig config) {
- final int defaultWordStyle = CompatChanges.isChangeEnabled(WORD_STYLE_AUTO)
- ? LINE_BREAK_WORD_STYLE_AUTO : LINE_BREAK_WORD_STYLE_NONE;
+ final int targetSdkVersion = ActivityThread.currentApplication().getApplicationInfo()
+ .targetSdkVersion;
+ final int defaultWordStyle;
+ final int vicVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM;
+ if (targetSdkVersion >= vicVersion) {
+ defaultWordStyle = LINE_BREAK_WORD_STYLE_AUTO;
+ } else {
+ defaultWordStyle = LINE_BREAK_WORD_STYLE_NONE;
+ }
if (config == null) {
return defaultWordStyle;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index bb433db..e7f6f0d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -17,7 +17,7 @@
package com.android.wm.shell.back;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME;
-import static com.android.window.flags.Flags.predictiveBackSystemAnimations;
+import static com.android.window.flags.Flags.predictiveBackSystemAnims;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
@@ -244,7 +244,7 @@
private void setupAnimationDeveloperSettingsObserver(
@NonNull ContentResolver contentResolver,
@NonNull @ShellBackgroundThread final Handler backgroundHandler) {
- if (predictiveBackSystemAnimations()) {
+ if (predictiveBackSystemAnims()) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation aconfig flag is enabled, therefore "
+ "developer settings flag is ignored and no content observer registered");
return;
@@ -267,7 +267,7 @@
*/
@ShellBackgroundThread
private void updateEnableAnimationFromFlags() {
- boolean isEnabled = predictiveBackSystemAnimations() || isDeveloperSettingEnabled();
+ boolean isEnabled = predictiveBackSystemAnims() || isDeveloperSettingEnabled();
mEnableAnimations.set(isEnabled);
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 1211451..bd8ce80 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -26,6 +26,7 @@
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.ColorDrawable;
+import android.view.Gravity;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewTreeObserver;
@@ -74,10 +75,6 @@
private DismissView mDismissView;
private @Nullable Consumer<String> mUnBubbleConversationCallback;
- // TODO(b/273310265) - currently the view is always on the right, need to update for RTL.
- /** Whether the expanded view is displaying on the left of the screen or not. */
- private boolean mOnLeft = false;
-
/** Whether a bubble is expanded. */
private boolean mIsExpanded = false;
@@ -154,10 +151,10 @@
return mIsExpanded;
}
- // (TODO: b/273310265): BubblePositioner should be source of truth when this work is done.
+ // TODO(b/313661121) - when dragging is implemented, check user setting first
/** Whether the expanded view is positioned on the left or right side of the screen. */
public boolean isOnLeft() {
- return mOnLeft;
+ return getLayoutDirection() == LAYOUT_DIRECTION_RTL;
}
/** Shows the expanded view of the provided bubble. */
@@ -216,7 +213,7 @@
return Unit.INSTANCE;
});
- addView(mExpandedView, new FrameLayout.LayoutParams(width, height));
+ addView(mExpandedView, new LayoutParams(width, height, Gravity.LEFT));
}
if (mEducationViewController.isEducationVisible()) {
@@ -311,7 +308,7 @@
lp.width = width;
lp.height = height;
mExpandedView.setLayoutParams(lp);
- if (mOnLeft) {
+ if (isOnLeft()) {
mExpandedView.setX(mPositioner.getInsets().left + padding);
} else {
mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
index 1c94625..54e162b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java
@@ -54,6 +54,7 @@
// Referenced in com.android.systemui.util.NotificationChannels.
public static final String NOTIFICATION_CHANNEL = "TVPIP";
private static final String NOTIFICATION_TAG = "TvPip";
+ private static final String EXTRA_COMPONENT_NAME = "TvPipComponentName";
private final Context mContext;
private final PackageManager mPackageManager;
@@ -176,6 +177,7 @@
Bundle extras = new Bundle();
extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken);
+ extras.putParcelable(EXTRA_COMPONENT_NAME, PipUtils.getTopPipActivity(mContext).first);
mNotificationBuilder.setExtras(extras);
PendingIntent closeIntent = mTvPipActionsProvider.getCloseAction().getPendingIntent();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index bf783e6..8c2203e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -21,17 +21,9 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
import static android.view.WindowManager.TRANSIT_PIP;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
-import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
-import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
-import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
-import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
-import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
import android.annotation.NonNull;
@@ -56,7 +48,6 @@
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.recents.RecentsTransitionHandler;
-import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.splitscreen.StageCoordinator;
import com.android.wm.shell.sysui.ShellInit;
@@ -84,7 +75,7 @@
private UnfoldTransitionHandler mUnfoldHandler;
private ActivityEmbeddingController mActivityEmbeddingController;
- private static class MixedTransition {
+ abstract static class MixedTransition {
static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
/** Both the display and split-state (enter/exit) is changing */
@@ -124,15 +115,11 @@
int mAnimType = ANIM_TYPE_DEFAULT;
final IBinder mTransition;
- private final Transitions mPlayer;
- private final DefaultMixedHandler mMixedHandler;
- private final PipTransitionController mPipHandler;
- private final RecentsTransitionHandler mRecentsHandler;
- private final StageCoordinator mSplitHandler;
- private final KeyguardTransitionHandler mKeyguardHandler;
- private final DesktopTasksController mDesktopTasksController;
- private final UnfoldTransitionHandler mUnfoldHandler;
- private final ActivityEmbeddingController mActivityEmbeddingController;
+ protected final Transitions mPlayer;
+ protected final DefaultMixedHandler mMixedHandler;
+ protected final PipTransitionController mPipHandler;
+ protected final StageCoordinator mSplitHandler;
+ protected final KeyguardTransitionHandler mKeyguardHandler;
Transitions.TransitionHandler mLeftoversHandler = null;
TransitionInfo mInfo = null;
@@ -156,409 +143,33 @@
MixedTransition(int type, IBinder transition, Transitions player,
DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
- RecentsTransitionHandler recentsHandler, StageCoordinator splitHandler,
- KeyguardTransitionHandler keyguardHandler,
- DesktopTasksController desktopTasksController,
- UnfoldTransitionHandler unfoldHandler,
- ActivityEmbeddingController activityEmbeddingController) {
+ StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler) {
mType = type;
mTransition = transition;
mPlayer = player;
mMixedHandler = mixedHandler;
mPipHandler = pipHandler;
- mRecentsHandler = recentsHandler;
mSplitHandler = splitHandler;
mKeyguardHandler = keyguardHandler;
- mDesktopTasksController = desktopTasksController;
- mUnfoldHandler = unfoldHandler;
- mActivityEmbeddingController = activityEmbeddingController;
-
- switch (type) {
- case TYPE_RECENTS_DURING_DESKTOP:
- case TYPE_RECENTS_DURING_KEYGUARD:
- case TYPE_RECENTS_DURING_SPLIT:
- mLeftoversHandler = mRecentsHandler;
- break;
- case TYPE_UNFOLD:
- mLeftoversHandler = mUnfoldHandler;
- break;
- case TYPE_DISPLAY_AND_SPLIT_CHANGE:
- case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
- case TYPE_ENTER_PIP_FROM_SPLIT:
- case TYPE_KEYGUARD:
- case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
- default:
- break;
- }
}
- boolean startAnimation(
+ abstract boolean startAnimation(
@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- switch (mType) {
- case TYPE_ENTER_PIP_FROM_SPLIT:
- return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
- finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
- case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
- return animateEnterPipFromActivityEmbedding(
- info, startTransaction, finishTransaction, finishCallback);
- case TYPE_DISPLAY_AND_SPLIT_CHANGE:
- return false;
- case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
- final boolean handledToPip = animateOpenIntentWithRemoteAndPip(
- info, startTransaction, finishTransaction, finishCallback);
- // Consume the transition on remote handler if the leftover handler already
- // handle this transition. And if it cannot, the transition will be handled by
- // remote handler, so don't consume here.
- // Need to check leftOverHandler as it may change in
- // #animateOpenIntentWithRemoteAndPip
- if (handledToPip && mHasRequestToRemote
- && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
- mPlayer.getRemoteTransitionHandler().onTransitionConsumed(
- transition, false, null);
- }
- return handledToPip;
- case TYPE_RECENTS_DURING_SPLIT:
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- final TransitionInfo.Change change = info.getChanges().get(i);
- // Pip auto-entering info might be appended to recent transition like
- // pressing home-key in 3-button navigation. This offers split handler the
- // opportunity to handle split to pip animation.
- if (mPipHandler.isEnteringPip(change, info.getType())
- && mSplitHandler.getSplitItemPosition(change.getLastParent())
- != SPLIT_POSITION_UNDEFINED) {
- return animateEnterPipFromSplit(
- this, info, startTransaction, finishTransaction, finishCallback,
- mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
- }
- }
+ @NonNull Transitions.TransitionFinishCallback finishCallback);
- return animateRecentsDuringSplit(
- info, startTransaction, finishTransaction, finishCallback);
- case TYPE_KEYGUARD:
- return animateKeyguard(this, info, startTransaction, finishTransaction,
- finishCallback, mKeyguardHandler, mPipHandler);
- case TYPE_RECENTS_DURING_KEYGUARD:
- return animateRecentsDuringKeyguard(
- info, startTransaction, finishTransaction, finishCallback);
- case TYPE_RECENTS_DURING_DESKTOP:
- return animateRecentsDuringDesktop(
- info, startTransaction, finishTransaction, finishCallback);
- case TYPE_UNFOLD:
- return animateUnfold(
- info, startTransaction, finishTransaction, finishCallback);
- default:
- throw new IllegalStateException(
- "Starting mixed animation without a known mixed type? " + mType);
- }
- }
-
- private boolean animateEnterPipFromActivityEmbedding(
- @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
- + "entering PIP from an Activity Embedding window");
- // Split into two transitions (wct)
- TransitionInfo.Change pipChange = null;
- final TransitionInfo everythingElse =
- subCopy(info, TRANSIT_TO_BACK, true /* changes */);
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- TransitionInfo.Change change = info.getChanges().get(i);
- if (mPipHandler.isEnteringPip(change, info.getType())) {
- if (pipChange != null) {
- throw new IllegalStateException("More than 1 pip-entering changes in one"
- + " transition? " + info);
- }
- pipChange = change;
- // going backwards, so remove-by-index is fine.
- everythingElse.getChanges().remove(i);
- }
- }
-
- final Transitions.TransitionFinishCallback finishCB = (wct) -> {
- --mInFlightSubAnimations;
- joinFinishArgs(wct);
- if (mInFlightSubAnimations > 0) return;
- finishCallback.onTransitionFinished(mFinishWCT);
- };
-
- if (!mActivityEmbeddingController.shouldAnimate(everythingElse)) {
- // Fallback to dispatching to other handlers.
- return false;
- }
-
- // PIP window should always be on the highest Z order.
- if (pipChange != null) {
- mInFlightSubAnimations = 2;
- mPipHandler.startEnterAnimation(
- pipChange,
- startTransaction.setLayer(pipChange.getLeash(), Integer.MAX_VALUE),
- finishTransaction,
- finishCB);
- } else {
- mInFlightSubAnimations = 1;
- }
-
- mActivityEmbeddingController.startAnimation(mTransition, everythingElse,
- startTransaction, finishTransaction, finishCB);
- return true;
- }
-
- private boolean animateOpenIntentWithRemoteAndPip(
- @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- TransitionInfo.Change pipChange = null;
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- TransitionInfo.Change change = info.getChanges().get(i);
- if (mPipHandler.isEnteringPip(change, info.getType())) {
- if (pipChange != null) {
- throw new IllegalStateException("More than 1 pip-entering changes in one"
- + " transition? " + info);
- }
- pipChange = change;
- info.getChanges().remove(i);
- }
- }
- Transitions.TransitionFinishCallback finishCB = (wct) -> {
- --mInFlightSubAnimations;
- joinFinishArgs(wct);
- if (mInFlightSubAnimations > 0) return;
- finishCallback.onTransitionFinished(mFinishWCT);
- };
- if (pipChange == null) {
- if (mLeftoversHandler != null) {
- mInFlightSubAnimations = 1;
- if (mLeftoversHandler.startAnimation(
- mTransition, info, startTransaction, finishTransaction, finishCB)) {
- return true;
- }
- }
- return false;
- }
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate"
- + " animation because remote-animation likely doesn't support it");
- // Split the transition into 2 parts: the pip part and the rest.
- mInFlightSubAnimations = 2;
- // make a new startTransaction because pip's startEnterAnimation "consumes" it so
- // we need a separate one to send over to launcher.
- SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
-
- mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
-
- // Dispatch the rest of the transition normally.
- if (mLeftoversHandler != null
- && mLeftoversHandler.startAnimation(
- mTransition, info, startTransaction, finishTransaction, finishCB)) {
- return true;
- }
- mLeftoversHandler = mPlayer.dispatchTransition(mTransition, info,
- startTransaction, finishTransaction, finishCB, mMixedHandler);
- return true;
- }
-
- private boolean animateRecentsDuringSplit(
- @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- // Split-screen is only interested in the recents transition finishing (and merging), so
- // just wrap finish and start recents animation directly.
- Transitions.TransitionFinishCallback finishCB = (wct) -> {
- mInFlightSubAnimations = 0;
- // If pair-to-pair switching, the post-recents clean-up isn't needed.
- wct = wct != null ? wct : new WindowContainerTransaction();
- if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
- mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
- } else {
- // notify pair-to-pair recents animation finish
- mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
- }
- mSplitHandler.onTransitionAnimationComplete();
- finishCallback.onTransitionFinished(wct);
- };
- mInFlightSubAnimations = 1;
- mSplitHandler.onRecentsInSplitAnimationStart(info);
- final boolean handled = mLeftoversHandler.startAnimation(mTransition, info,
- startTransaction, finishTransaction, finishCB);
- if (!handled) {
- mSplitHandler.onRecentsInSplitAnimationCanceled();
- }
- return handled;
- }
-
- private boolean animateRecentsDuringKeyguard(
- @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (mInfo == null) {
- mInfo = info;
- mFinishT = finishTransaction;
- mFinishCB = finishCallback;
- }
- return startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction);
- }
-
- private boolean animateRecentsDuringDesktop(
- @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- Transitions.TransitionFinishCallback finishCB = wct -> {
- mInFlightSubAnimations--;
- if (mInFlightSubAnimations == 0) {
- finishCallback.onTransitionFinished(wct);
- }
- };
-
- mInFlightSubAnimations++;
- boolean consumed = mRecentsHandler.startAnimation(
- mTransition, info, startTransaction, finishTransaction, finishCB);
- if (!consumed) {
- mInFlightSubAnimations--;
- return false;
- }
- if (mDesktopTasksController != null) {
- mDesktopTasksController.syncSurfaceState(info, finishTransaction);
- return true;
- }
-
- return false;
- }
-
- private boolean animateUnfold(
- @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- final Transitions.TransitionFinishCallback finishCB = (wct) -> {
- mInFlightSubAnimations--;
- if (mInFlightSubAnimations > 0) return;
- finishCallback.onTransitionFinished(wct);
- };
- mInFlightSubAnimations = 1;
- // Sync pip state.
- if (mPipHandler != null) {
- mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
- }
- if (mSplitHandler != null && mSplitHandler.isSplitActive()) {
- mSplitHandler.updateSurfaces(startTransaction);
- }
- return mUnfoldHandler.startAnimation(
- mTransition, info, startTransaction, finishTransaction, finishCB);
- }
-
- void mergeAnimation(
+ abstract void mergeAnimation(
@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- switch (mType) {
- case TYPE_DISPLAY_AND_SPLIT_CHANGE:
- // queue since no actual animation.
- break;
- case TYPE_ENTER_PIP_FROM_SPLIT:
- if (mAnimType == ANIM_TYPE_GOING_HOME) {
- boolean ended = mSplitHandler.end();
- // If split couldn't end (because it is remote), then don't end everything
- // else since we have to play out the animation anyways.
- if (!ended) return;
- mPipHandler.end();
- if (mLeftoversHandler != null) {
- mLeftoversHandler.mergeAnimation(
- transition, info, t, mergeTarget, finishCallback);
- }
- } else {
- mPipHandler.end();
- }
- break;
- case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
- mPipHandler.end();
- mActivityEmbeddingController.mergeAnimation(transition, info, t, mergeTarget,
- finishCallback);
- break;
- case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
- mPipHandler.end();
- if (mLeftoversHandler != null) {
- mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
- finishCallback);
- }
- break;
- case TYPE_RECENTS_DURING_SPLIT:
- if (mSplitHandler.isPendingEnter(transition)) {
- // Recents -> enter-split means that we are switching from one pair to
- // another pair.
- mAnimType = ANIM_TYPE_PAIR_TO_PAIR;
- }
- mLeftoversHandler.mergeAnimation(
- transition, info, t, mergeTarget, finishCallback);
- break;
- case TYPE_KEYGUARD:
- mKeyguardHandler.mergeAnimation(
- transition, info, t, mergeTarget, finishCallback);
- break;
- case TYPE_RECENTS_DURING_KEYGUARD:
- if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
- DefaultMixedHandler.handoverTransitionLeashes(mInfo, info, t, mFinishT);
- if (animateKeyguard(this, info, t, mFinishT, mFinishCB, mKeyguardHandler,
- mPipHandler)) {
- finishCallback.onTransitionFinished(null);
- }
- }
- mLeftoversHandler.mergeAnimation(
- transition, info, t, mergeTarget, finishCallback);
- break;
- case TYPE_RECENTS_DURING_DESKTOP:
- mLeftoversHandler.mergeAnimation(
- transition, info, t, mergeTarget, finishCallback);
- break;
- case TYPE_UNFOLD:
- mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
- break;
- default:
- throw new IllegalStateException(
- "Playing a mixed transition with unknown type? " + mType);
- }
- }
+ @NonNull Transitions.TransitionFinishCallback finishCallback);
- void onTransitionConsumed(
+ abstract void onTransitionConsumed(
@NonNull IBinder transition, boolean aborted,
- @Nullable SurfaceControl.Transaction finishT) {
- switch (mType) {
- case TYPE_ENTER_PIP_FROM_SPLIT:
- mPipHandler.onTransitionConsumed(transition, aborted, finishT);
- break;
- case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
- mPipHandler.onTransitionConsumed(transition, aborted, finishT);
- mActivityEmbeddingController.onTransitionConsumed(transition, aborted, finishT);
- break;
- case TYPE_RECENTS_DURING_SPLIT:
- case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
- case TYPE_RECENTS_DURING_DESKTOP:
- mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
- break;
- case TYPE_KEYGUARD:
- mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
- break;
- case TYPE_UNFOLD:
- mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
- break;
- default:
- break;
- }
+ @Nullable SurfaceControl.Transaction finishT);
- if (mHasRequestToRemote) {
- mPlayer.getRemoteTransitionHandler().onTransitionConsumed(
- transition, aborted, finishT);
- }
- }
-
- boolean startSubAnimation(Transitions.TransitionHandler handler, TransitionInfo info,
+ protected boolean startSubAnimation(
+ Transitions.TransitionHandler handler, TransitionInfo info,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
if (mInfo != null) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
@@ -573,7 +184,7 @@
return true;
}
- void onSubAnimationFinished(TransitionInfo info, WindowContainerTransaction wct) {
+ private void onSubAnimationFinished(TransitionInfo info, WindowContainerTransaction wct) {
mInFlightSubAnimations--;
if (mInfo != null) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
@@ -644,7 +255,7 @@
throw new IllegalStateException("Unexpected remote transition in"
+ "pip-enter-from-split request");
}
- mActiveTransitions.add(createMixedTransition(
+ mActiveTransitions.add(createDefaultMixedTransition(
MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition));
WindowContainerTransaction out = new WindowContainerTransaction();
@@ -656,7 +267,7 @@
mActivityEmbeddingController != null)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
" Got a PiP-enter request from an Activity Embedding split");
- mActiveTransitions.add(createMixedTransition(
+ mActiveTransitions.add(createDefaultMixedTransition(
MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING, transition));
// Postpone transition splitting to later.
WindowContainerTransaction out = new WindowContainerTransaction();
@@ -675,7 +286,7 @@
if (handler == null) {
return null;
}
- final MixedTransition mixed = createMixedTransition(
+ final MixedTransition mixed = createDefaultMixedTransition(
MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition);
mixed.mLeftoversHandler = handler.first;
mActiveTransitions.add(mixed);
@@ -701,7 +312,7 @@
mPlayer.getRemoteTransitionHandler(),
new WindowContainerTransaction());
}
- final MixedTransition mixed = createMixedTransition(
+ final MixedTransition mixed = createRecentsMixedTransition(
MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
mixed.mLeftoversHandler = handler.first;
mActiveTransitions.add(mixed);
@@ -710,7 +321,7 @@
final WindowContainerTransaction wct =
mUnfoldHandler.handleRequest(transition, request);
if (wct != null) {
- mActiveTransitions.add(createMixedTransition(
+ mActiveTransitions.add(createDefaultMixedTransition(
MixedTransition.TYPE_UNFOLD, transition));
}
return wct;
@@ -718,6 +329,12 @@
return null;
}
+ private DefaultMixedTransition createDefaultMixedTransition(int type, IBinder transition) {
+ return new DefaultMixedTransition(
+ type, transition, mPlayer, this, mPipHandler, mSplitHandler, mKeyguardHandler,
+ mUnfoldHandler, mActivityEmbeddingController);
+ }
+
@Override
public Consumer<IBinder> handleRecentsRequest(WindowContainerTransaction outWCT) {
if (mRecentsHandler != null) {
@@ -737,31 +354,30 @@
private void setRecentsTransitionDuringSplit(IBinder transition) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+ "Split-Screen is foreground, so treat it as Mixed.");
- mActiveTransitions.add(createMixedTransition(
+ mActiveTransitions.add(createRecentsMixedTransition(
MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition));
}
private void setRecentsTransitionDuringKeyguard(IBinder transition) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+ "keyguard is visible, so treat it as Mixed.");
- mActiveTransitions.add(createMixedTransition(
+ mActiveTransitions.add(createRecentsMixedTransition(
MixedTransition.TYPE_RECENTS_DURING_KEYGUARD, transition));
}
private void setRecentsTransitionDuringDesktop(IBinder transition) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+ "desktop mode is active, so treat it as Mixed.");
- mActiveTransitions.add(createMixedTransition(
+ mActiveTransitions.add(createRecentsMixedTransition(
MixedTransition.TYPE_RECENTS_DURING_DESKTOP, transition));
}
- private MixedTransition createMixedTransition(int type, IBinder transition) {
- return new MixedTransition(type, transition, mPlayer, this, mPipHandler, mRecentsHandler,
- mSplitHandler, mKeyguardHandler, mDesktopTasksController, mUnfoldHandler,
- mActivityEmbeddingController);
+ private MixedTransition createRecentsMixedTransition(int type, IBinder transition) {
+ return new RecentsMixedTransition(type, transition, mPlayer, this, mPipHandler,
+ mSplitHandler, mKeyguardHandler, mRecentsHandler, mDesktopTasksController);
}
- private static TransitionInfo subCopy(@NonNull TransitionInfo info,
+ static TransitionInfo subCopy(@NonNull TransitionInfo info,
@WindowManager.TransitionType int newType, boolean withChanges) {
final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0);
out.setTrack(info.getTrack());
@@ -778,15 +394,6 @@
return out;
}
- private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
- return change.getTaskInfo() != null
- && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
- }
-
- private static boolean isWallpaper(@NonNull TransitionInfo.Change change) {
- return (change.getFlags() & FLAG_IS_WALLPAPER) != 0;
- }
-
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@@ -805,7 +412,7 @@
if (KeyguardTransitionHandler.handles(info)) {
if (mixed != null && mixed.mType != MixedTransition.TYPE_KEYGUARD) {
final MixedTransition keyguardMixed =
- createMixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
+ createDefaultMixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
mActiveTransitions.add(keyguardMixed);
Transitions.TransitionFinishCallback callback = wct -> {
mActiveTransitions.remove(keyguardMixed);
@@ -845,117 +452,6 @@
return handled;
}
- private static boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed,
- @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback,
- @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler,
- @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
- + "entering PIP while Split-Screen is foreground.");
- TransitionInfo.Change pipChange = null;
- TransitionInfo.Change wallpaper = null;
- final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
- boolean homeIsOpening = false;
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- TransitionInfo.Change change = info.getChanges().get(i);
- if (pipHandler.isEnteringPip(change, info.getType())) {
- if (pipChange != null) {
- throw new IllegalStateException("More than 1 pip-entering changes in one"
- + " transition? " + info);
- }
- pipChange = change;
- // going backwards, so remove-by-index is fine.
- everythingElse.getChanges().remove(i);
- } else if (isHomeOpening(change)) {
- homeIsOpening = true;
- } else if (isWallpaper(change)) {
- wallpaper = change;
- }
- }
- if (pipChange == null) {
- // um, something probably went wrong.
- return false;
- }
- final boolean isGoingHome = homeIsOpening;
- Transitions.TransitionFinishCallback finishCB = (wct) -> {
- --mixed.mInFlightSubAnimations;
- mixed.joinFinishArgs(wct);
- if (mixed.mInFlightSubAnimations > 0) return;
- if (isGoingHome) {
- splitHandler.onTransitionAnimationComplete();
- }
- finishCallback.onTransitionFinished(mixed.mFinishWCT);
- };
- if (isGoingHome || splitHandler.getSplitItemPosition(pipChange.getLastParent())
- != SPLIT_POSITION_UNDEFINED) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed "
- + "since entering-PiP caused us to leave split and return home.");
- // We need to split the transition into 2 parts: the pip part (animated by pip)
- // and the dismiss-part (animated by launcher).
- mixed.mInFlightSubAnimations = 2;
- // immediately make the wallpaper visible (so that we don't see it pop-in during
- // the time it takes to start recents animation (which is remote).
- if (wallpaper != null) {
- startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f);
- }
- // make a new startTransaction because pip's startEnterAnimation "consumes" it so
- // we need a separate one to send over to launcher.
- SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
- @SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED;
- if (splitHandler.isSplitScreenVisible()) {
- // The non-going home case, we could be pip-ing one of the split stages and keep
- // showing the other
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- TransitionInfo.Change change = info.getChanges().get(i);
- if (change == pipChange) {
- // Ignore the change/task that's going into Pip
- continue;
- }
- @SplitScreen.StageType int splitItemStage =
- splitHandler.getSplitItemStage(change.getLastParent());
- if (splitItemStage != STAGE_TYPE_UNDEFINED) {
- topStageToKeep = splitItemStage;
- break;
- }
- }
- }
- // Let split update internal state for dismiss.
- splitHandler.prepareDismissAnimation(topStageToKeep,
- EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT,
- finishTransaction);
-
- // We are trying to accommodate launcher's close animation which can't handle the
- // divider-bar, so if split-handler is closing the divider-bar, just hide it and remove
- // from transition info.
- for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) {
- if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR) != 0) {
- everythingElse.getChanges().remove(i);
- break;
- }
- }
-
- pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA);
- pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
- finishCB);
- // Dispatch the rest of the transition normally. This will most-likely be taken by
- // recents or default handler.
- mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse,
- otherStartT, finishTransaction, finishCB, mixedHandler);
- } else {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just "
- + "forward animation to Pip-Handler.");
- // This happens if the pip-ing activity is in a multi-activity task (and thus a
- // new pip task is spawned). In this case, we don't actually exit split so we can
- // just let pip transition handle the animation verbatim.
- mixed.mInFlightSubAnimations = 1;
- pipHandler.startAnimation(mixed.mTransition, info, startTransaction, finishTransaction,
- finishCB);
- }
- return true;
- }
-
private void unlinkMissingParents(TransitionInfo from) {
for (int i = 0; i < from.getChanges().size(); ++i) {
final TransitionInfo.Change chg = from.getChanges().get(i);
@@ -987,15 +483,14 @@
public boolean animatePendingEnterPipFromSplit(IBinder transition, TransitionInfo info,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
Transitions.TransitionFinishCallback finishCallback) {
- final MixedTransition mixed = createMixedTransition(
+ final MixedTransition mixed = createDefaultMixedTransition(
MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition);
mActiveTransitions.add(mixed);
Transitions.TransitionFinishCallback callback = wct -> {
mActiveTransitions.remove(mixed);
finishCallback.onTransitionFinished(wct);
};
- return animateEnterPipFromSplit(mixed, info, startT, finishT, finishCallback, mPlayer, this,
- mPipHandler, mSplitHandler);
+ return mixed.startAnimation(transition, info, startT, finishT, callback);
}
/**
@@ -1018,7 +513,7 @@
}
if (displayPart.getChanges().isEmpty()) return false;
unlinkMissingParents(everythingElse);
- final MixedTransition mixed = createMixedTransition(
+ final MixedTransition mixed = createDefaultMixedTransition(
MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition);
mActiveTransitions.add(mixed);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change "
@@ -1135,7 +630,7 @@
* {@link TransitionInfo} so that it can take over some parts of the animation without
* reparenting to new transition roots.
*/
- private static void handoverTransitionLeashes(
+ static void handoverTransitionLeashes(
@NonNull TransitionInfo from,
@NonNull TransitionInfo to,
@NonNull SurfaceControl.Transaction startT,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
new file mode 100644
index 0000000..9ce46d6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2023 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.wm.shell.transition;
+
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+
+import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeyguard;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+import com.android.wm.shell.unfold.UnfoldTransitionHandler;
+
+class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
+ private final UnfoldTransitionHandler mUnfoldHandler;
+ private final ActivityEmbeddingController mActivityEmbeddingController;
+
+ DefaultMixedTransition(int type, IBinder transition, Transitions player,
+ DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
+ StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler,
+ UnfoldTransitionHandler unfoldHandler,
+ ActivityEmbeddingController activityEmbeddingController) {
+ super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler);
+ mUnfoldHandler = unfoldHandler;
+ mActivityEmbeddingController = activityEmbeddingController;
+
+ switch (type) {
+ case TYPE_UNFOLD:
+ mLeftoversHandler = mUnfoldHandler;
+ break;
+ case TYPE_DISPLAY_AND_SPLIT_CHANGE:
+ case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+ case TYPE_ENTER_PIP_FROM_SPLIT:
+ case TYPE_KEYGUARD:
+ case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+ default:
+ break;
+ }
+ }
+
+ @Override
+ boolean startAnimation(
+ @NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ return switch (mType) {
+ case TYPE_DISPLAY_AND_SPLIT_CHANGE -> false;
+ case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING ->
+ animateEnterPipFromActivityEmbedding(
+ info, startTransaction, finishTransaction, finishCallback);
+ case TYPE_ENTER_PIP_FROM_SPLIT ->
+ animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
+ finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
+ case TYPE_KEYGUARD ->
+ animateKeyguard(this, info, startTransaction, finishTransaction, finishCallback,
+ mKeyguardHandler, mPipHandler);
+ case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE ->
+ animateOpenIntentWithRemoteAndPip(transition, info, startTransaction,
+ finishTransaction, finishCallback);
+ case TYPE_UNFOLD ->
+ animateUnfold(info, startTransaction, finishTransaction, finishCallback);
+ default -> throw new IllegalStateException(
+ "Starting default mixed animation with unknown or illegal type: " + mType);
+ };
+ }
+
+ private boolean animateEnterPipFromActivityEmbedding(
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
+ + "entering PIP from an Activity Embedding window");
+ // Split into two transitions (wct)
+ TransitionInfo.Change pipChange = null;
+ final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ TransitionInfo.Change change = info.getChanges().get(i);
+ if (mPipHandler.isEnteringPip(change, info.getType())) {
+ if (pipChange != null) {
+ throw new IllegalStateException("More than 1 pip-entering changes in one"
+ + " transition? " + info);
+ }
+ pipChange = change;
+ // going backwards, so remove-by-index is fine.
+ everythingElse.getChanges().remove(i);
+ }
+ }
+
+ final Transitions.TransitionFinishCallback finishCB = (wct) -> {
+ --mInFlightSubAnimations;
+ joinFinishArgs(wct);
+ if (mInFlightSubAnimations > 0) return;
+ finishCallback.onTransitionFinished(mFinishWCT);
+ };
+
+ if (!mActivityEmbeddingController.shouldAnimate(everythingElse)) {
+ // Fallback to dispatching to other handlers.
+ return false;
+ }
+
+ // PIP window should always be on the highest Z order.
+ if (pipChange != null) {
+ mInFlightSubAnimations = 2;
+ mPipHandler.startEnterAnimation(
+ pipChange, startTransaction.setLayer(pipChange.getLeash(), Integer.MAX_VALUE),
+ finishTransaction,
+ finishCB);
+ } else {
+ mInFlightSubAnimations = 1;
+ }
+
+ mActivityEmbeddingController.startAnimation(
+ mTransition, everythingElse, startTransaction, finishTransaction, finishCB);
+ return true;
+ }
+
+ private boolean animateOpenIntentWithRemoteAndPip(
+ @NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ boolean handledToPip = tryAnimateOpenIntentWithRemoteAndPip(
+ info, startTransaction, finishTransaction, finishCallback);
+ // Consume the transition on remote handler if the leftover handler already handle this
+ // transition. And if it cannot, the transition will be handled by remote handler, so don't
+ // consume here.
+ // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip
+ if (handledToPip && mHasRequestToRemote
+ && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
+ mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null);
+ }
+ return handledToPip;
+ }
+
+ private boolean tryAnimateOpenIntentWithRemoteAndPip(
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ TransitionInfo.Change pipChange = null;
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ TransitionInfo.Change change = info.getChanges().get(i);
+ if (mPipHandler.isEnteringPip(change, info.getType())) {
+ if (pipChange != null) {
+ throw new IllegalStateException("More than 1 pip-entering changes in one"
+ + " transition? " + info);
+ }
+ pipChange = change;
+ info.getChanges().remove(i);
+ }
+ }
+ Transitions.TransitionFinishCallback finishCB = (wct) -> {
+ --mInFlightSubAnimations;
+ joinFinishArgs(wct);
+ if (mInFlightSubAnimations > 0) return;
+ finishCallback.onTransitionFinished(mFinishWCT);
+ };
+ if (pipChange == null) {
+ if (mLeftoversHandler != null) {
+ mInFlightSubAnimations = 1;
+ if (mLeftoversHandler.startAnimation(
+ mTransition, info, startTransaction, finishTransaction, finishCB)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate"
+ + " animation because remote-animation likely doesn't support it");
+ // Split the transition into 2 parts: the pip part and the rest.
+ mInFlightSubAnimations = 2;
+ // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+ // we need a separate one to send over to launcher.
+ SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+
+ mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
+
+ // Dispatch the rest of the transition normally.
+ if (mLeftoversHandler != null
+ && mLeftoversHandler.startAnimation(mTransition, info,
+ startTransaction, finishTransaction, finishCB)) {
+ return true;
+ }
+ mLeftoversHandler = mPlayer.dispatchTransition(
+ mTransition, info, startTransaction, finishTransaction, finishCB, mMixedHandler);
+ return true;
+ }
+
+ private boolean animateUnfold(
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ final Transitions.TransitionFinishCallback finishCB = (wct) -> {
+ mInFlightSubAnimations--;
+ if (mInFlightSubAnimations > 0) return;
+ finishCallback.onTransitionFinished(wct);
+ };
+ mInFlightSubAnimations = 1;
+ // Sync pip state.
+ if (mPipHandler != null) {
+ mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
+ }
+ if (mSplitHandler != null && mSplitHandler.isSplitActive()) {
+ mSplitHandler.updateSurfaces(startTransaction);
+ }
+ return mUnfoldHandler.startAnimation(
+ mTransition, info, startTransaction, finishTransaction, finishCB);
+ }
+
+ @Override
+ void mergeAnimation(
+ @NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ switch (mType) {
+ case TYPE_DISPLAY_AND_SPLIT_CHANGE:
+ // queue since no actual animation.
+ return;
+ case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+ mPipHandler.end();
+ mActivityEmbeddingController.mergeAnimation(
+ transition, info, t, mergeTarget, finishCallback);
+ return;
+ case TYPE_ENTER_PIP_FROM_SPLIT:
+ if (mAnimType == ANIM_TYPE_GOING_HOME) {
+ boolean ended = mSplitHandler.end();
+ // If split couldn't end (because it is remote), then don't end everything else
+ // since we have to play out the animation anyways.
+ if (!ended) return;
+ mPipHandler.end();
+ if (mLeftoversHandler != null) {
+ mLeftoversHandler.mergeAnimation(
+ transition, info, t, mergeTarget, finishCallback);
+ }
+ } else {
+ mPipHandler.end();
+ }
+ return;
+ case TYPE_KEYGUARD:
+ mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+ return;
+ case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+ mPipHandler.end();
+ if (mLeftoversHandler != null) {
+ mLeftoversHandler.mergeAnimation(
+ transition, info, t, mergeTarget, finishCallback);
+ }
+ return;
+ case TYPE_UNFOLD:
+ mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+ return;
+ default:
+ throw new IllegalStateException("Playing a default mixed transition with unknown or"
+ + " illegal type: " + mType);
+ }
+ }
+
+ @Override
+ void onTransitionConsumed(
+ @NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishT) {
+ switch (mType) {
+ case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+ mPipHandler.onTransitionConsumed(transition, aborted, finishT);
+ mActivityEmbeddingController.onTransitionConsumed(transition, aborted, finishT);
+ break;
+ case TYPE_ENTER_PIP_FROM_SPLIT:
+ mPipHandler.onTransitionConsumed(transition, aborted, finishT);
+ break;
+ case TYPE_KEYGUARD:
+ mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
+ break;
+ case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+ mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
+ break;
+ case TYPE_UNFOLD:
+ mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
+ break;
+ default:
+ break;
+ }
+
+ if (mHasRequestToRemote) {
+ mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
new file mode 100644
index 0000000..0974cd1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2023 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.wm.shell.transition;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
+import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy;
+
+import android.annotation.NonNull;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.splitscreen.SplitScreen;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+
+public class MixedTransitionHelper {
+ static boolean animateEnterPipFromSplit(
+ @NonNull DefaultMixedHandler.MixedTransition mixed, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback,
+ @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler,
+ @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
+ + "entering PIP while Split-Screen is foreground.");
+ TransitionInfo.Change pipChange = null;
+ TransitionInfo.Change wallpaper = null;
+ final TransitionInfo everythingElse =
+ subCopy(info, TRANSIT_TO_BACK, true /* changes */);
+ boolean homeIsOpening = false;
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ TransitionInfo.Change change = info.getChanges().get(i);
+ if (pipHandler.isEnteringPip(change, info.getType())) {
+ if (pipChange != null) {
+ throw new IllegalStateException("More than 1 pip-entering changes in one"
+ + " transition? " + info);
+ }
+ pipChange = change;
+ // going backwards, so remove-by-index is fine.
+ everythingElse.getChanges().remove(i);
+ } else if (isHomeOpening(change)) {
+ homeIsOpening = true;
+ } else if (isWallpaper(change)) {
+ wallpaper = change;
+ }
+ }
+ if (pipChange == null) {
+ // um, something probably went wrong.
+ return false;
+ }
+ final boolean isGoingHome = homeIsOpening;
+ Transitions.TransitionFinishCallback finishCB = (wct) -> {
+ --mixed.mInFlightSubAnimations;
+ mixed.joinFinishArgs(wct);
+ if (mixed.mInFlightSubAnimations > 0) return;
+ if (isGoingHome) {
+ splitHandler.onTransitionAnimationComplete();
+ }
+ finishCallback.onTransitionFinished(mixed.mFinishWCT);
+ };
+ if (isGoingHome || splitHandler.getSplitItemPosition(pipChange.getLastParent())
+ != SPLIT_POSITION_UNDEFINED) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed "
+ + "since entering-PiP caused us to leave split and return home.");
+ // We need to split the transition into 2 parts: the pip part (animated by pip)
+ // and the dismiss-part (animated by launcher).
+ mixed.mInFlightSubAnimations = 2;
+ // immediately make the wallpaper visible (so that we don't see it pop-in during
+ // the time it takes to start recents animation (which is remote).
+ if (wallpaper != null) {
+ startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f);
+ }
+ // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+ // we need a separate one to send over to launcher.
+ SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+ @SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED;
+ if (splitHandler.isSplitScreenVisible()) {
+ // The non-going home case, we could be pip-ing one of the split stages and keep
+ // showing the other
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ TransitionInfo.Change change = info.getChanges().get(i);
+ if (change == pipChange) {
+ // Ignore the change/task that's going into Pip
+ continue;
+ }
+ @SplitScreen.StageType int splitItemStage =
+ splitHandler.getSplitItemStage(change.getLastParent());
+ if (splitItemStage != STAGE_TYPE_UNDEFINED) {
+ topStageToKeep = splitItemStage;
+ break;
+ }
+ }
+ }
+ // Let split update internal state for dismiss.
+ splitHandler.prepareDismissAnimation(topStageToKeep,
+ EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT,
+ finishTransaction);
+
+ // We are trying to accommodate launcher's close animation which can't handle the
+ // divider-bar, so if split-handler is closing the divider-bar, just hide it and
+ // remove from transition info.
+ for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) {
+ if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR)
+ != 0) {
+ everythingElse.getChanges().remove(i);
+ break;
+ }
+ }
+
+ pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA);
+ pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
+ finishCB);
+ // Dispatch the rest of the transition normally. This will most-likely be taken by
+ // recents or default handler.
+ mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse,
+ otherStartT, finishTransaction, finishCB, mixedHandler);
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just "
+ + "forward animation to Pip-Handler.");
+ // This happens if the pip-ing activity is in a multi-activity task (and thus a
+ // new pip task is spawned). In this case, we don't actually exit split so we can
+ // just let pip transition handle the animation verbatim.
+ mixed.mInFlightSubAnimations = 1;
+ pipHandler.startAnimation(
+ mixed.mTransition, info, startTransaction, finishTransaction, finishCB);
+ }
+ return true;
+ }
+
+ private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
+ return change.getTaskInfo() != null
+ && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
+ }
+
+ private static boolean isWallpaper(@NonNull TransitionInfo.Change change) {
+ return (change.getFlags() & FLAG_IS_WALLPAPER) != 0;
+ }
+
+ static boolean animateKeyguard(
+ @NonNull DefaultMixedHandler.MixedTransition mixed, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback,
+ @NonNull KeyguardTransitionHandler keyguardHandler,
+ PipTransitionController pipHandler) {
+ if (mixed.mFinishT == null) {
+ mixed.mFinishT = finishTransaction;
+ mixed.mFinishCB = finishCallback;
+ }
+ // Sync pip state.
+ if (pipHandler != null) {
+ pipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
+ }
+ return mixed.startSubAnimation(keyguardHandler, info, startTransaction, finishTransaction);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
new file mode 100644
index 0000000..643e026
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2023 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.wm.shell.transition;
+
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.transition.DefaultMixedHandler.handoverTransitionLeashes;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeyguard;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.desktopmode.DesktopTasksController;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+
+class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
+ private final RecentsTransitionHandler mRecentsHandler;
+ private final DesktopTasksController mDesktopTasksController;
+
+ RecentsMixedTransition(int type, IBinder transition, Transitions player,
+ DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
+ StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler,
+ RecentsTransitionHandler recentsHandler,
+ DesktopTasksController desktopTasksController) {
+ super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler);
+ mRecentsHandler = recentsHandler;
+ mDesktopTasksController = desktopTasksController;
+ mLeftoversHandler = mRecentsHandler;
+ }
+
+ @Override
+ boolean startAnimation(
+ @NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ return switch (mType) {
+ case TYPE_RECENTS_DURING_DESKTOP ->
+ animateRecentsDuringDesktop(
+ info, startTransaction, finishTransaction, finishCallback);
+ case TYPE_RECENTS_DURING_KEYGUARD ->
+ animateRecentsDuringKeyguard(
+ info, startTransaction, finishTransaction, finishCallback);
+ case TYPE_RECENTS_DURING_SPLIT ->
+ animateRecentsDuringSplit(
+ info, startTransaction, finishTransaction, finishCallback);
+ default -> throw new IllegalStateException(
+ "Starting Recents mixed animation with unknown or illegal type: " + mType);
+ };
+ }
+
+ private boolean animateRecentsDuringDesktop(
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (mInfo == null) {
+ mInfo = info;
+ mFinishT = finishTransaction;
+ mFinishCB = finishCallback;
+ }
+ Transitions.TransitionFinishCallback finishCB = wct -> {
+ mInFlightSubAnimations--;
+ if (mInFlightSubAnimations == 0) {
+ finishCallback.onTransitionFinished(wct);
+ }
+ };
+
+ mInFlightSubAnimations++;
+ boolean consumed = mRecentsHandler.startAnimation(
+ mTransition, info, startTransaction, finishTransaction, finishCB);
+ if (!consumed) {
+ mInFlightSubAnimations--;
+ return false;
+ }
+ if (mDesktopTasksController != null) {
+ mDesktopTasksController.syncSurfaceState(info, finishTransaction);
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean animateRecentsDuringKeyguard(
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (mInfo == null) {
+ mInfo = info;
+ mFinishT = finishTransaction;
+ mFinishCB = finishCallback;
+ }
+ return startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction);
+ }
+
+ private boolean animateRecentsDuringSplit(
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ // Pip auto-entering info might be appended to recent transition like pressing
+ // home-key in 3-button navigation. This offers split handler the opportunity to
+ // handle split to pip animation.
+ if (mPipHandler.isEnteringPip(change, info.getType())
+ && mSplitHandler.getSplitItemPosition(change.getLastParent())
+ != SPLIT_POSITION_UNDEFINED) {
+ return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
+ finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
+ }
+ }
+
+ // Split-screen is only interested in the recents transition finishing (and merging), so
+ // just wrap finish and start recents animation directly.
+ Transitions.TransitionFinishCallback finishCB = (wct) -> {
+ mInFlightSubAnimations = 0;
+ // If pair-to-pair switching, the post-recents clean-up isn't needed.
+ wct = wct != null ? wct : new WindowContainerTransaction();
+ if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
+ mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
+ } else {
+ // notify pair-to-pair recents animation finish
+ mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
+ }
+ mSplitHandler.onTransitionAnimationComplete();
+ finishCallback.onTransitionFinished(wct);
+ };
+ mInFlightSubAnimations = 1;
+ mSplitHandler.onRecentsInSplitAnimationStart(info);
+ final boolean handled = mLeftoversHandler.startAnimation(
+ mTransition, info, startTransaction, finishTransaction, finishCB);
+ if (!handled) {
+ mSplitHandler.onRecentsInSplitAnimationCanceled();
+ }
+ return handled;
+ }
+
+ @Override
+ void mergeAnimation(
+ @NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ switch (mType) {
+ case TYPE_RECENTS_DURING_DESKTOP:
+ mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+ return;
+ case TYPE_RECENTS_DURING_KEYGUARD:
+ if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
+ handoverTransitionLeashes(mInfo, info, t, mFinishT);
+ if (animateKeyguard(
+ this, info, t, mFinishT, mFinishCB, mKeyguardHandler, mPipHandler)) {
+ finishCallback.onTransitionFinished(null);
+ }
+ }
+ mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+ finishCallback);
+ return;
+ case TYPE_RECENTS_DURING_SPLIT:
+ if (mSplitHandler.isPendingEnter(transition)) {
+ // Recents -> enter-split means that we are switching from one pair to
+ // another pair.
+ mAnimType = DefaultMixedHandler.MixedTransition.ANIM_TYPE_PAIR_TO_PAIR;
+ }
+ mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+ return;
+ default:
+ throw new IllegalStateException("Playing a Recents mixed transition with unknown or"
+ + " illegal type: " + mType);
+ }
+ }
+
+ @Override
+ void onTransitionConsumed(
+ @NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishT) {
+ switch (mType) {
+ case TYPE_RECENTS_DURING_DESKTOP:
+ case TYPE_RECENTS_DURING_SPLIT:
+ mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
+ break;
+ default:
+ break;
+ }
+
+ if (mHasRequestToRemote) {
+ mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT);
+ }
+ }
+}
diff --git a/libs/hostgraphics/gui/Surface.h b/libs/hostgraphics/gui/Surface.h
index de1ba00..2573931 100644
--- a/libs/hostgraphics/gui/Surface.h
+++ b/libs/hostgraphics/gui/Surface.h
@@ -50,6 +50,8 @@
virtual int unlockAndPost() { return 0; }
virtual int query(int what, int* value) const { return 0; }
+ virtual void destroy() {}
+
protected:
virtual ~Surface() {}
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index eebf8aa..b40b73c 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -716,7 +716,6 @@
],
shared_libs: [
"libmemunreachable",
- "server_configurable_flags",
],
srcs: [
"tests/unit/main.cpp",
diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h
index d4af087..a5a841e 100644
--- a/libs/hwui/DeviceInfo.h
+++ b/libs/hwui/DeviceInfo.h
@@ -23,6 +23,7 @@
#include <mutex>
+#include "Properties.h"
#include "utils/Macros.h"
namespace android {
@@ -60,7 +61,13 @@
static void setWideColorDataspace(ADataSpace dataspace);
static void setSupportFp16ForHdr(bool supportFp16ForHdr);
- static bool isSupportFp16ForHdr() { return get()->mSupportFp16ForHdr; };
+ static bool isSupportFp16ForHdr() {
+ if (!Properties::hdr10bitPlus) {
+ return false;
+ }
+
+ return get()->mSupportFp16ForHdr;
+ };
static void setSupportMixedColorSpaces(bool supportMixedColorSpaces);
static bool isSupportMixedColorSpaces() { return get()->mSupportMixedColorSpaces; };
diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp
index 16de21d..71f7926 100644
--- a/libs/hwui/HardwareBitmapUploader.cpp
+++ b/libs/hwui/HardwareBitmapUploader.cpp
@@ -379,7 +379,7 @@
case kAlpha_8_SkColorType:
formatInfo.isSupported = HardwareBitmapUploader::hasAlpha8Support();
formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8_UNORM;
- formatInfo.format = GL_R8;
+ formatInfo.format = GL_RED;
formatInfo.type = GL_UNSIGNED_BYTE;
formatInfo.vkFormat = VK_FORMAT_R8_UNORM;
break;
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index d58c872..755332ff 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -38,6 +38,9 @@
constexpr bool clip_surfaceviews() {
return false;
}
+constexpr bool hdr_10bit_plus() {
+ return false;
+}
} // namespace hwui_flags
#endif
@@ -105,6 +108,7 @@
float Properties::maxHdrHeadroomOn8bit = 5.f; // TODO: Refine this number
bool Properties::clipSurfaceViews = false;
+bool Properties::hdr10bitPlus = false;
StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI;
@@ -177,6 +181,7 @@
clipSurfaceViews =
base::GetBoolProperty("debug.hwui.clip_surfaceviews", hwui_flags::clip_surfaceviews());
+ hdr10bitPlus = hwui_flags::hdr_10bit_plus();
return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw);
}
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index b956fac..ec53070 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -336,6 +336,7 @@
static float maxHdrHeadroomOn8bit;
static bool clipSurfaceViews;
+ static bool hdr10bitPlus;
static StretchEffectBehavior getStretchEffectBehavior() {
return stretchEffectBehavior;
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index 58d9d8b..286f06a 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -578,16 +578,6 @@
return result;
}
- // This method is kept for old Robolectric JNI signature used by SystemUIGoogleRoboRNGTests.
- static jfloat getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric(
- JNIEnv* env, jclass cls, jlong paintHandle, jcharArray text, jint start, jint end,
- jint contextStart, jint contextEnd, jboolean isRtl, jint offset, jfloatArray advances,
- jint advancesIndex, jobject drawBounds) {
- return getRunCharacterAdvance___CIIIIZI_FI_F(env, cls, paintHandle, text, start, end,
- contextStart, contextEnd, isRtl, offset,
- advances, advancesIndex, drawBounds, nullptr);
- }
-
static jint doOffsetForAdvance(const Paint* paint, const Typeface* typeface, const jchar buf[],
jint start, jint count, jint bufSize, jboolean isRtl, jfloat advance) {
minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
@@ -1163,8 +1153,6 @@
{"nGetRunCharacterAdvance",
"(J[CIIIIZI[FILandroid/graphics/RectF;Landroid/graphics/Paint$RunInfo;)F",
(void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F},
- {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;)F",
- (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric},
{"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I},
{"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
(void*)PaintGlue::getFontMetricsIntForText___C},
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
index e0f1f6e..326b6ed 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -650,9 +650,14 @@
mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace();
break;
case ColorMode::Hdr:
- mSurfaceColorType = SkColorType::kN32_SkColorType;
- mSurfaceColorSpace = SkColorSpace::MakeRGB(
- GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
+ if (DeviceInfo::get()->isSupportFp16ForHdr()) {
+ mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType;
+ mSurfaceColorSpace = SkColorSpace::MakeSRGB();
+ } else {
+ mSurfaceColorType = SkColorType::kN32_SkColorType;
+ mSurfaceColorSpace = SkColorSpace::MakeRGB(
+ GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
+ }
break;
case ColorMode::Hdr10:
mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType;
@@ -669,8 +674,13 @@
void SkiaPipeline::setTargetSdrHdrRatio(float ratio) {
if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) {
mTargetSdrHdrRatio = ratio;
- mSurfaceColorSpace = SkColorSpace::MakeRGB(GetExtendedTransferFunction(mTargetSdrHdrRatio),
- SkNamedGamut::kDisplayP3);
+
+ if (mColorMode == ColorMode::Hdr && DeviceInfo::get()->isSupportFp16ForHdr()) {
+ mSurfaceColorSpace = SkColorSpace::MakeSRGB();
+ } else {
+ mSurfaceColorSpace = SkColorSpace::MakeRGB(
+ GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
+ }
} else {
mTargetSdrHdrRatio = 1.f;
}
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index facf30b..2904dfe 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -441,22 +441,32 @@
colorMode = ColorMode::Default;
}
- if (DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType) {
+ // TODO: maybe we want to get rid of the WCG check if overlay properties just works?
+ const bool canUseFp16 = DeviceInfo::get()->isSupportFp16ForHdr() ||
+ DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType;
+
+ if (canUseFp16) {
if (mEglConfigF16 == EGL_NO_CONFIG_KHR) {
colorMode = ColorMode::Default;
} else {
config = mEglConfigF16;
}
}
+
if (EglExtensions.glColorSpace) {
attribs[0] = EGL_GL_COLORSPACE_KHR;
switch (colorMode) {
case ColorMode::Default:
attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR;
break;
+ case ColorMode::Hdr:
+ if (canUseFp16) {
+ attribs[1] = EGL_GL_COLORSPACE_SCRGB_EXT;
+ break;
+ // No fp16 support so fallthrough to HDR10
+ }
// We don't have an EGL colorspace for extended range P3 that's used for HDR
// So override it after configuring the EGL context
- case ColorMode::Hdr:
case ColorMode::Hdr10:
overrideWindowDataSpaceForHdr = true;
attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT;
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index 46db777..587e35b 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -16,6 +16,9 @@
package android.media;
+import static com.android.media.codec.flags.Flags.FLAG_CODEC_IMPORTANCE;
+
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -1635,6 +1638,34 @@
*/
public static final String KEY_ALLOW_FRAME_DROP = "allow-frame-drop";
+ /**
+ * A key describing the desired codec importance for the application.
+ * <p>
+ * The associated value is a positive integer including zero.
+ * Higher value means lesser importance.
+ * <p>
+ * The resource manager may use the codec importance, along with other factors
+ * when reclaiming codecs from an application.
+ * The specifics of reclaim policy is device dependent, but specifying the codec importance,
+ * will allow the resource manager to prioritize reclaiming less important codecs
+ * (assigned higher values) from the (reclaim) requesting application first.
+ * So, the codec importance is only relevant within the context of that application.
+ * <p>
+ * The codec importance can be set:
+ * <ul>
+ * <li>through {@link MediaCodec#configure}. </li>
+ * <li>through {@link MediaCodec#setParameters} if the codec has been configured already,
+ * which allows the users to change the codec importance multiple times.
+ * </ul>
+ * Any change/update in codec importance is guaranteed upon the completion of the function call
+ * that sets the codec importance. So, in case of concurrent codec operations,
+ * make sure to wait for the change in codec importance, before using another codec.
+ * Note that unless specified, by default the codecs will have highest importance (of value 0).
+ *
+ */
+ @FlaggedApi(FLAG_CODEC_IMPORTANCE)
+ public static final String KEY_IMPORTANCE = "importance";
+
/* package private */ MediaFormat(@NonNull Map<String, Object> map) {
mMap = map;
}
diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl
index 8ce1b6d..3d927d3 100644
--- a/media/java/android/media/projection/IMediaProjectionManager.aidl
+++ b/media/java/android/media/projection/IMediaProjectionManager.aidl
@@ -121,7 +121,7 @@
@EnforcePermission("MANAGE_MEDIA_PROJECTION")
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
- void addCallback(IMediaProjectionWatcherCallback callback);
+ MediaProjectionInfo addCallback(IMediaProjectionWatcherCallback callback);
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
diff --git a/media/java/android/media/projection/MediaProjectionInfo.java b/media/java/android/media/projection/MediaProjectionInfo.java
index ff60856..c820392 100644
--- a/media/java/android/media/projection/MediaProjectionInfo.java
+++ b/media/java/android/media/projection/MediaProjectionInfo.java
@@ -16,6 +16,7 @@
package android.media.projection;
+import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
@@ -26,15 +27,18 @@
public final class MediaProjectionInfo implements Parcelable {
private final String mPackageName;
private final UserHandle mUserHandle;
+ private final IBinder mLaunchCookie;
- public MediaProjectionInfo(String packageName, UserHandle handle) {
+ public MediaProjectionInfo(String packageName, UserHandle handle, IBinder launchCookie) {
mPackageName = packageName;
mUserHandle = handle;
+ mLaunchCookie = launchCookie;
}
public MediaProjectionInfo(Parcel in) {
mPackageName = in.readString();
mUserHandle = UserHandle.readFromParcel(in);
+ mLaunchCookie = in.readStrongBinder();
}
public String getPackageName() {
@@ -45,12 +49,16 @@
return mUserHandle;
}
+ public IBinder getLaunchCookie() {
+ return mLaunchCookie;
+ }
+
@Override
public boolean equals(Object o) {
- if (o instanceof MediaProjectionInfo) {
- final MediaProjectionInfo other = (MediaProjectionInfo) o;
+ if (o instanceof MediaProjectionInfo other) {
return Objects.equals(other.mPackageName, mPackageName)
- && Objects.equals(other.mUserHandle, mUserHandle);
+ && Objects.equals(other.mUserHandle, mUserHandle)
+ && Objects.equals(other.mLaunchCookie, mLaunchCookie);
}
return false;
}
@@ -64,7 +72,8 @@
public String toString() {
return "MediaProjectionInfo{mPackageName="
+ mPackageName + ", mUserHandle="
- + mUserHandle + "}";
+ + mUserHandle + ", mLaunchCookie"
+ + mLaunchCookie + "}";
}
@Override
@@ -76,6 +85,7 @@
public void writeToParcel(Parcel out, int flags) {
out.writeString(mPackageName);
UserHandle.writeToParcel(mUserHandle, out);
+ out.writeStrongBinder(mLaunchCookie);
}
public static final @android.annotation.NonNull Parcelable.Creator<MediaProjectionInfo> CREATOR =
diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java
index db01950..7f8f1a3 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -16,6 +16,7 @@
package android.media.tv;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -29,6 +30,7 @@
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
+import android.media.tv.flags.Flags;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
@@ -2540,9 +2542,9 @@
* <p>This is used to indicate the broadcast visibility type defined in the underlying
* broadcast standard or country/operator profile, if applicable. For example,
* {@code visible_service_flag} and {@code numeric_selection_flag} of
- * {@code service_attribute_descriptor} in D-Book, {@code visible_service_flag} and
- * {@code selectable_service_flag} of {@code ciplus_service_descriptor} in CI Plus 1.3
- * specification.
+ * {@code service_attribute_descriptor} in D-Book, the specification for UK-based TV
+ * products, {@code visible_service_flag} and {@code selectable_service_flag} of
+ * {@code ciplus_service_descriptor} in the CI Plus 1.3 specification.
*
* <p>The value should match one of the following:
* {@link #BROADCAST_VISIBILITY_TYPE_VISIBLE},
@@ -2553,8 +2555,8 @@
* by default.
*
* <p>Type: INTEGER
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES)
public static final String COLUMN_BROADCAST_VISIBILITY_TYPE = "broadcast_visibility_type";
/** @hide */
@@ -2571,8 +2573,8 @@
* visible from users and selectable by users via normal service navigation mechanisms.
*
* @see #COLUMN_BROADCAST_VISIBILITY_TYPE
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES)
public static final int BROADCAST_VISIBILITY_TYPE_VISIBLE = 0;
/**
@@ -2581,18 +2583,18 @@
* the logical channel number.
*
* @see #COLUMN_BROADCAST_VISIBILITY_TYPE
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES)
public static final int BROADCAST_VISIBILITY_TYPE_NUMERIC_SELECTABLE_ONLY = 1;
/**
* The broadcast visibility type for invisible services. Use this type when the service
- * is invisible from users and unselectable by users via any of normal service navigation
- * mechanisms.
+ * is invisible from users and not able to be selected by users via any of the normal
+ * service navigation mechanisms.
*
* @see #COLUMN_BROADCAST_VISIBILITY_TYPE
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES)
public static final int BROADCAST_VISIBILITY_TYPE_INVISIBLE = 2;
private Channels() {}
diff --git a/media/java/android/media/tv/TvTrackInfo.java b/media/java/android/media/tv/TvTrackInfo.java
index 78d7d76..2ebb19a 100644
--- a/media/java/android/media/tv/TvTrackInfo.java
+++ b/media/java/android/media/tv/TvTrackInfo.java
@@ -55,6 +55,27 @@
*/
public static final int TYPE_SUBTITLE = 2;
+ /**
+ * The component tag identifies a component carried by a MPEG-2 TS.
+ *
+ * This corresponds to the component_tag in the component descriptor in the
+ * Elementary Stream loop of the stream in the Program Map Table
+ * (PMT) [EN 300 468], or undefined if the component is not carried in an
+ * MPEG-2 TS.
+ *
+ * @hide
+ */
+ public static final String EXTRA_BUNDLE_KEY_COMPONENT_TAG = "component_tag";
+
+ /**
+ * The MPEG Program ID (PID) of the component in the MPEG2-TS in
+ * which it is carried, or undefined if the component is not carried in an
+ * MPEG-2 TS.
+ *
+ * @hide
+ */
+ public static final String EXTRA_BUNDLE_KEY_PID = "pid";
+
private final int mType;
private final String mId;
private final String mLanguage;
diff --git a/media/java/android/media/tv/ad/ITvAdClient.aidl b/media/java/android/media/tv/ad/ITvAdClient.aidl
new file mode 100644
index 0000000..34d96b3
--- /dev/null
+++ b/media/java/android/media/tv/ad/ITvAdClient.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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.media.tv.ad;
+
+import android.view.InputChannel;
+
+/**
+ * Interface a client of the ITvAdManager implements, to identify itself and receive
+ * information about changes to the state of each TV AD service.
+ * @hide
+ */
+oneway interface ITvAdClient {
+ void onSessionCreated(in String serviceId, IBinder token, in InputChannel channel, int seq);
+ void onSessionReleased(int seq);
+ void onLayoutSurface(int left, int top, int right, int bottom, int seq);
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl
index 92cc923..a747e49 100644
--- a/media/java/android/media/tv/ad/ITvAdManager.aidl
+++ b/media/java/android/media/tv/ad/ITvAdManager.aidl
@@ -16,10 +16,25 @@
package android.media.tv.ad;
+import android.media.tv.ad.ITvAdClient;
+import android.media.tv.ad.ITvAdManagerCallback;
+import android.media.tv.ad.TvAdServiceInfo;
+import android.view.Surface;
+
/**
* Interface to the TV AD service.
* @hide
*/
interface ITvAdManager {
+ List<TvAdServiceInfo> getTvAdServiceList(int userId);
+ void createSession(
+ in ITvAdClient client, in String serviceId, in String type, int seq, int userId);
+ void releaseSession(in IBinder sessionToken, int userId);
void startAdService(in IBinder sessionToken, int userId);
+ void setSurface(in IBinder sessionToken, in Surface surface, int userId);
+ void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height,
+ int userId);
+
+ void registerCallback(in ITvAdManagerCallback callback, int userId);
+ void unregisterCallback(in ITvAdManagerCallback callback, int userId);
}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl
similarity index 67%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to media/java/android/media/tv/ad/ITvAdManagerCallback.aidl
index ce92b6d..f55f67e 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl
@@ -13,10 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.companion.virtual.camera;
+
+package android.media.tv.ad;
/**
- * The configuration of a single virtual camera stream.
+ * Interface to receive callbacks from ITvAdManager regardless of sessions.
* @hide
*/
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+oneway interface ITvAdManagerCallback {
+ void onAdServiceAdded(in String serviceId);
+ void onAdServiceRemoved(in String serviceId);
+ void onAdServiceUpdated(in String serviceId);
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdService.aidl b/media/java/android/media/tv/ad/ITvAdService.aidl
new file mode 100644
index 0000000..3bb0409
--- /dev/null
+++ b/media/java/android/media/tv/ad/ITvAdService.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 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.media.tv.ad;
+
+import android.media.tv.ad.ITvAdServiceCallback;
+import android.media.tv.ad.ITvAdSessionCallback;
+import android.os.Bundle;
+import android.view.InputChannel;
+
+/**
+ * Top-level interface to a TV AD component (implemented in a Service). It's used for
+ * TvAdManagerService to communicate with TvAdService.
+ * @hide
+ */
+oneway interface ITvAdService {
+ void registerCallback(in ITvAdServiceCallback callback);
+ void unregisterCallback(in ITvAdServiceCallback callback);
+ void createSession(in InputChannel channel, in ITvAdSessionCallback callback,
+ in String serviceId, in String type);
+ void sendAppLinkCommand(in Bundle command);
+}
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl
similarity index 78%
copy from core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
copy to media/java/android/media/tv/ad/ITvAdServiceCallback.aidl
index ce92b6d..a087181 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl
@@ -13,10 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.companion.virtual.camera;
+
+package android.media.tv.ad;
/**
- * The configuration of a single virtual camera stream.
+ * Helper interface for ITvAdService to allow the TvAdService to notify the TvAdManagerService.
* @hide
*/
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
+oneway interface ITvAdServiceCallback {
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl
index b834f1b9..751257c 100644
--- a/media/java/android/media/tv/ad/ITvAdSession.aidl
+++ b/media/java/android/media/tv/ad/ITvAdSession.aidl
@@ -16,10 +16,15 @@
package android.media.tv.ad;
+import android.view.Surface;
+
/**
- * Sub-interface of ITvAdService which is created per session and has its own context.
+ * Sub-interface of ITvAdService.aidl which is created per session and has its own context.
* @hide
*/
oneway interface ITvAdSession {
+ void release();
void startAdService();
+ void setSurface(in Surface surface);
+ void dispatchSurfaceChanged(int format, int width, int height);
}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItem.java b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
similarity index 63%
copy from core/java/android/hardware/biometrics/PromptContentListItem.java
copy to media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
index fa3783d..f21ef19 100644
--- a/core/java/android/hardware/biometrics/PromptContentListItem.java
+++ b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl
@@ -14,16 +14,16 @@
* limitations under the License.
*/
-package android.hardware.biometrics;
+package android.media.tv.ad;
-import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
-
-import android.annotation.FlaggedApi;
+import android.media.tv.ad.ITvAdSession;
/**
- * A list item shown on {@link PromptVerticalListContentView}.
+ * Helper interface for ITvAdSession to allow TvAdService to notify the system service when there is
+ * a related event.
+ * @hide
*/
-@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
-public interface PromptContentListItem {
-}
-
+oneway interface ITvAdSessionCallback {
+ void onSessionCreated(in ITvAdSession session);
+ void onLayoutSurface(int left, int top, int right, int bottom);
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
new file mode 100644
index 0000000..4df2783
--- /dev/null
+++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 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.media.tv.ad;
+
+import android.content.Context;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.Surface;
+
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+
+/**
+ * Implements the internal ITvAdSession interface.
+ * @hide
+ */
+public class ITvAdSessionWrapper
+ extends ITvAdSession.Stub implements HandlerCaller.Callback {
+
+ private static final String TAG = "ITvAdSessionWrapper";
+
+ private static final int EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS = 1000;
+ private static final int EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS = 5 * 1000;
+ private static final int DO_RELEASE = 1;
+ private static final int DO_SET_SURFACE = 2;
+ private static final int DO_DISPATCH_SURFACE_CHANGED = 3;
+
+ private final HandlerCaller mCaller;
+ private TvAdService.Session mSessionImpl;
+ private InputChannel mChannel;
+ private TvAdEventReceiver mReceiver;
+
+ public ITvAdSessionWrapper(
+ Context context, TvAdService.Session mSessionImpl, InputChannel channel) {
+ this.mSessionImpl = mSessionImpl;
+ mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */);
+ mChannel = channel;
+ if (channel != null) {
+ mReceiver = new TvAdEventReceiver(channel, context.getMainLooper());
+ }
+ }
+
+ @Override
+ public void release() {
+ mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE));
+ }
+
+
+ @Override
+ public void executeMessage(Message msg) {
+ if (mSessionImpl == null) {
+ return;
+ }
+
+ long startTime = System.nanoTime();
+ switch (msg.what) {
+ case DO_RELEASE: {
+ mSessionImpl.release();
+ mSessionImpl = null;
+ if (mReceiver != null) {
+ mReceiver.dispose();
+ mReceiver = null;
+ }
+ if (mChannel != null) {
+ mChannel.dispose();
+ mChannel = null;
+ }
+ break;
+ }
+ case DO_SET_SURFACE: {
+ mSessionImpl.setSurface((Surface) msg.obj);
+ break;
+ }
+ case DO_DISPATCH_SURFACE_CHANGED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mSessionImpl.dispatchSurfaceChanged(
+ (Integer) args.argi1, (Integer) args.argi2, (Integer) args.argi3);
+ args.recycle();
+ break;
+ }
+ default: {
+ Log.w(TAG, "Unhandled message code: " + msg.what);
+ break;
+ }
+ }
+ long durationMs = (System.nanoTime() - startTime) / (1000 * 1000);
+ if (durationMs > EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS) {
+ Log.w(TAG, "Handling message (" + msg.what + ") took too long time (duration="
+ + durationMs + "ms)");
+ if (durationMs > EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS) {
+ // TODO: handle timeout
+ }
+ }
+
+ }
+
+ @Override
+ public void startAdService() throws RemoteException {
+
+ }
+
+ @Override
+ public void setSurface(Surface surface) {
+ mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface));
+ }
+
+ @Override
+ public void dispatchSurfaceChanged(int format, int width, int height) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageIIII(DO_DISPATCH_SURFACE_CHANGED, format, width, height, 0));
+ }
+
+ private final class TvAdEventReceiver extends InputEventReceiver {
+ TvAdEventReceiver(InputChannel inputChannel, Looper looper) {
+ super(inputChannel, looper);
+ }
+
+ @Override
+ public void onInputEvent(InputEvent event) {
+ if (mSessionImpl == null) {
+ // The session has been finished.
+ finishInputEvent(event, false);
+ return;
+ }
+
+ int handled = mSessionImpl.dispatchInputEvent(event, this);
+ if (handled != TvAdManager.Session.DISPATCH_IN_PROGRESS) {
+ finishInputEvent(
+ event, handled == TvAdManager.Session.DISPATCH_HANDLED);
+ }
+ }
+ }
+}
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
index 2b52c4b..9c75051 100644
--- a/media/java/android/media/tv/ad/TvAdManager.java
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -17,12 +17,30 @@
package android.media.tv.ad;
import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemService;
import android.content.Context;
+import android.media.tv.TvInputManager;
import android.media.tv.flags.Flags;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
+import android.util.Pools;
+import android.util.SparseArray;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventSender;
+import android.view.Surface;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
/**
* Central system API to the overall client-side TV AD architecture, which arbitrates interaction
@@ -37,10 +55,163 @@
private final ITvAdManager mService;
private final int mUserId;
+ // A mapping from the sequence number of a session to its SessionCallbackRecord.
+ private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap =
+ new SparseArray<>();
+
+ // @GuardedBy("mLock")
+ private final List<TvAdServiceCallbackRecord> mCallbackRecords = new ArrayList<>();
+
+ // A sequence number for the next session to be created. Should be protected by a lock
+ // {@code mSessionCallbackRecordMap}.
+ private int mNextSeq;
+
+ private final Object mLock = new Object();
+ private final ITvAdClient mClient;
+
/** @hide */
public TvAdManager(ITvAdManager service, int userId) {
mService = service;
mUserId = userId;
+ mClient = new ITvAdClient.Stub() {
+ @Override
+ public void onSessionCreated(String serviceId, IBinder token, InputChannel channel,
+ int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for " + token);
+ return;
+ }
+ Session session = null;
+ if (token != null) {
+ session = new Session(token, channel, mService, mUserId, seq,
+ mSessionCallbackRecordMap);
+ } else {
+ mSessionCallbackRecordMap.delete(seq);
+ }
+ record.postSessionCreated(session);
+ }
+ }
+
+ @Override
+ public void onSessionReleased(int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ mSessionCallbackRecordMap.delete(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq:" + seq);
+ return;
+ }
+ record.mSession.releaseInternal();
+ record.postSessionReleased();
+ }
+ }
+
+ @Override
+ public void onLayoutSurface(int left, int top, int right, int bottom, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postLayoutSurface(left, top, right, bottom);
+ }
+ }
+
+ };
+
+ ITvAdManagerCallback managerCallback =
+ new ITvAdManagerCallback.Stub() {
+ @Override
+ public void onAdServiceAdded(String serviceId) {
+ synchronized (mLock) {
+ for (TvAdServiceCallbackRecord record : mCallbackRecords) {
+ record.postAdServiceAdded(serviceId);
+ }
+ }
+ }
+
+ @Override
+ public void onAdServiceRemoved(String serviceId) {
+ synchronized (mLock) {
+ for (TvAdServiceCallbackRecord record : mCallbackRecords) {
+ record.postAdServiceRemoved(serviceId);
+ }
+ }
+ }
+
+ @Override
+ public void onAdServiceUpdated(String serviceId) {
+ synchronized (mLock) {
+ for (TvAdServiceCallbackRecord record : mCallbackRecords) {
+ record.postAdServiceUpdated(serviceId);
+ }
+ }
+ }
+ };
+ try {
+ if (mService != null) {
+ mService.registerCallback(managerCallback, mUserId);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the complete list of TV AD service on the system.
+ *
+ * @return List of {@link TvAdServiceInfo} for each TV AD service that describes its meta
+ * information.
+ * @hide
+ */
+ @NonNull
+ public List<TvAdServiceInfo> getTvAdServiceList() {
+ try {
+ return mService.getTvAdServiceList(mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Creates a {@link Session} for a given TV AD service.
+ *
+ * <p>The number of sessions that can be created at the same time is limited by the capability
+ * of the given AD service.
+ *
+ * @param serviceId The ID of the AD service.
+ * @param callback A callback used to receive the created session.
+ * @param handler A {@link Handler} that the session creation will be delivered to.
+ * @hide
+ */
+ public void createSession(
+ @NonNull String serviceId,
+ @NonNull String type,
+ @NonNull final TvAdManager.SessionCallback callback,
+ @NonNull Handler handler) {
+ createSessionInternal(serviceId, type, callback, handler);
+ }
+
+ private void createSessionInternal(String serviceId, String type,
+ TvAdManager.SessionCallback callback, Handler handler) {
+ Preconditions.checkNotNull(serviceId);
+ Preconditions.checkNotNull(type);
+ Preconditions.checkNotNull(callback);
+ Preconditions.checkNotNull(handler);
+ TvAdManager.SessionCallbackRecord
+ record = new TvAdManager.SessionCallbackRecord(callback, handler);
+ synchronized (mSessionCallbackRecordMap) {
+ int seq = mNextSeq++;
+ mSessionCallbackRecordMap.put(seq, record);
+ try {
+ mService.createSession(mClient, serviceId, type, seq, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
/**
@@ -48,14 +219,121 @@
* @hide
*/
public static final class Session {
- private final IBinder mToken;
+ static final int DISPATCH_IN_PROGRESS = -1;
+ static final int DISPATCH_NOT_HANDLED = 0;
+ static final int DISPATCH_HANDLED = 1;
+
+ private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500;
private final ITvAdManager mService;
private final int mUserId;
+ private final int mSeq;
+ private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap;
- private Session(IBinder token, ITvAdManager service, int userId) {
+ // For scheduling input event handling on the main thread. This also serves as a lock to
+ // protect pending input events and the input channel.
+ private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper());
+
+ private TvInputManager.Session mInputSession;
+ private final Pools.Pool<PendingEvent> mPendingEventPool = new Pools.SimplePool<>(20);
+ private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<>(20);
+ private TvInputEventSender mSender;
+ private InputChannel mInputChannel;
+ private IBinder mToken;
+
+ private Session(IBinder token, InputChannel channel, ITvAdManager service, int userId,
+ int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) {
mToken = token;
+ mInputChannel = channel;
mService = service;
mUserId = userId;
+ mSeq = seq;
+ mSessionCallbackRecordMap = sessionCallbackRecordMap;
+ }
+
+ /**
+ * Releases this session.
+ */
+ public void release() {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.releaseSession(mToken, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ releaseInternal();
+ }
+
+ /**
+ * Sets the {@link android.view.Surface} for this session.
+ *
+ * @param surface A {@link android.view.Surface} used to render AD.
+ */
+ public void setSurface(Surface surface) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ // surface can be null.
+ try {
+ mService.setSurface(mToken, surface, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Notifies of any structural changes (format or size) of the surface passed in
+ * {@link #setSurface}.
+ *
+ * @param format The new PixelFormat of the surface.
+ * @param width The new width of the surface.
+ * @param height The new height of the surface.
+ */
+ public void dispatchSurfaceChanged(int format, int width, int height) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private void flushPendingEventsLocked() {
+ mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT);
+
+ final int count = mPendingEvents.size();
+ for (int i = 0; i < count; i++) {
+ int seq = mPendingEvents.keyAt(i);
+ Message msg = mHandler.obtainMessage(
+ InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0);
+ msg.setAsynchronous(true);
+ msg.sendToTarget();
+ }
+ }
+
+ private void releaseInternal() {
+ mToken = null;
+ synchronized (mHandler) {
+ if (mInputChannel != null) {
+ if (mSender != null) {
+ flushPendingEventsLocked();
+ mSender.dispose();
+ mSender = null;
+ }
+ mInputChannel.dispose();
+ mInputChannel = null;
+ }
+ }
+ synchronized (mSessionCallbackRecordMap) {
+ mSessionCallbackRecordMap.delete(mSeq);
+ }
}
void startAdService() {
@@ -69,5 +347,324 @@
throw e.rethrowFromSystemServer();
}
}
+
+ private final class InputEventHandler extends Handler {
+ public static final int MSG_SEND_INPUT_EVENT = 1;
+ public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
+ public static final int MSG_FLUSH_INPUT_EVENT = 3;
+
+ InputEventHandler(Looper looper) {
+ super(looper, null, true);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SEND_INPUT_EVENT: {
+ sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj);
+ return;
+ }
+ case MSG_TIMEOUT_INPUT_EVENT: {
+ finishedInputEvent(msg.arg1, false, true);
+ return;
+ }
+ case MSG_FLUSH_INPUT_EVENT: {
+ finishedInputEvent(msg.arg1, false, false);
+ return;
+ }
+ }
+ }
+ }
+
+ // Assumes the event has already been removed from the queue.
+ void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
+ p.mHandled = handled;
+ if (p.mEventHandler.getLooper().isCurrentThread()) {
+ // Already running on the callback handler thread so we can send the callback
+ // immediately.
+ p.run();
+ } else {
+ // Post the event to the callback handler thread.
+ // In this case, the callback will be responsible for recycling the event.
+ Message msg = Message.obtain(p.mEventHandler, p);
+ msg.setAsynchronous(true);
+ msg.sendToTarget();
+ }
+ }
+
+ // Must be called on the main looper
+ private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) {
+ synchronized (mHandler) {
+ int result = sendInputEventOnMainLooperLocked(p);
+ if (result == DISPATCH_IN_PROGRESS) {
+ return;
+ }
+ }
+
+ invokeFinishedInputEventCallback(p, false);
+ }
+
+ private int sendInputEventOnMainLooperLocked(PendingEvent p) {
+ if (mInputChannel != null) {
+ if (mSender == null) {
+ mSender = new TvInputEventSender(mInputChannel, mHandler.getLooper());
+ }
+
+ final InputEvent event = p.mEvent;
+ final int seq = event.getSequenceNumber();
+ if (mSender.sendInputEvent(seq, event)) {
+ mPendingEvents.put(seq, p);
+ Message msg = mHandler.obtainMessage(
+ InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
+ msg.setAsynchronous(true);
+ mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT);
+ return DISPATCH_IN_PROGRESS;
+ }
+
+ Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:"
+ + event);
+ }
+ return DISPATCH_NOT_HANDLED;
+ }
+
+ void finishedInputEvent(int seq, boolean handled, boolean timeout) {
+ final PendingEvent p;
+ synchronized (mHandler) {
+ int index = mPendingEvents.indexOfKey(seq);
+ if (index < 0) {
+ return; // spurious, event already finished or timed out
+ }
+
+ p = mPendingEvents.valueAt(index);
+ mPendingEvents.removeAt(index);
+
+ if (timeout) {
+ Log.w(TAG, "Timeout waiting for session to handle input event after "
+ + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken);
+ } else {
+ mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
+ }
+ }
+
+ invokeFinishedInputEventCallback(p, handled);
+ }
+
+ private void recyclePendingEventLocked(PendingEvent p) {
+ p.recycle();
+ mPendingEventPool.release(p);
+ }
+
+ /**
+ * Callback that is invoked when an input event that was dispatched to this session has been
+ * finished.
+ *
+ * @hide
+ */
+ public interface FinishedInputEventCallback {
+ /**
+ * Called when the dispatched input event is finished.
+ *
+ * @param token A token passed to {@link #dispatchInputEvent}.
+ * @param handled {@code true} if the dispatched input event was handled properly.
+ * {@code false} otherwise.
+ */
+ void onFinishedInputEvent(Object token, boolean handled);
+ }
+
+ private final class TvInputEventSender extends InputEventSender {
+ TvInputEventSender(InputChannel inputChannel, Looper looper) {
+ super(inputChannel, looper);
+ }
+
+ @Override
+ public void onInputEventFinished(int seq, boolean handled) {
+ finishedInputEvent(seq, handled, false);
+ }
+ }
+
+ private final class PendingEvent implements Runnable {
+ public InputEvent mEvent;
+ public Object mEventToken;
+ public Session.FinishedInputEventCallback mCallback;
+ public Handler mEventHandler;
+ public boolean mHandled;
+
+ public void recycle() {
+ mEvent = null;
+ mEventToken = null;
+ mCallback = null;
+ mEventHandler = null;
+ mHandled = false;
+ }
+
+ @Override
+ public void run() {
+ mCallback.onFinishedInputEvent(mEventToken, mHandled);
+
+ synchronized (mEventHandler) {
+ recyclePendingEventLocked(this);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Interface used to receive the created session.
+ * @hide
+ */
+ public abstract static class SessionCallback {
+ /**
+ * This is called after {@link TvAdManager#createSession} has been processed.
+ *
+ * @param session A {@link TvAdManager.Session} instance created. This can be
+ * {@code null} if the creation request failed.
+ */
+ public void onSessionCreated(@Nullable Session session) {
+ }
+
+ /**
+ * This is called when {@link TvAdManager.Session} is released.
+ * This typically happens when the process hosting the session has crashed or been killed.
+ *
+ * @param session the {@link TvAdManager.Session} instance released.
+ */
+ public void onSessionReleased(@NonNull Session session) {
+ }
+
+ /**
+ * This is called when {@link TvAdService.Session#layoutSurface} is called to
+ * change the layout of surface.
+ *
+ * @param session A {@link TvAdManager.Session} associated with this callback.
+ * @param left Left position.
+ * @param top Top position.
+ * @param right Right position.
+ * @param bottom Bottom position.
+ */
+ public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
+ }
+
+ }
+
+ /**
+ * Callback used to monitor status of the TV AD service.
+ * @hide
+ */
+ public abstract static class TvAdServiceCallback {
+ /**
+ * This is called when a TV AD service is added to the system.
+ *
+ * <p>Normally it happens when the user installs a new TV AD service package that implements
+ * {@link TvAdService} interface.
+ *
+ * @param serviceId The ID of the TV AD service.
+ */
+ public void onAdServiceAdded(@NonNull String serviceId) {
+ }
+
+ /**
+ * This is called when a TV AD service is removed from the system.
+ *
+ * <p>Normally it happens when the user uninstalls the previously installed TV AD service
+ * package.
+ *
+ * @param serviceId The ID of the TV AD service.
+ */
+ public void onAdServiceRemoved(@NonNull String serviceId) {
+ }
+
+ /**
+ * This is called when a TV AD service is updated on the system.
+ *
+ * <p>Normally it happens when a previously installed TV AD service package is re-installed
+ * or a newer version of the package exists becomes available/unavailable.
+ *
+ * @param serviceId The ID of the TV AD service.
+ */
+ public void onAdServiceUpdated(@NonNull String serviceId) {
+ }
+
+ }
+
+ private static final class SessionCallbackRecord {
+ private final SessionCallback mSessionCallback;
+ private final Handler mHandler;
+ private Session mSession;
+
+ SessionCallbackRecord(SessionCallback sessionCallback, Handler handler) {
+ mSessionCallback = sessionCallback;
+ mHandler = handler;
+ }
+
+ void postSessionCreated(final Session session) {
+ mSession = session;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onSessionCreated(session);
+ }
+ });
+ }
+
+ void postSessionReleased() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onSessionReleased(mSession);
+ }
+ });
+ }
+
+ void postLayoutSurface(final int left, final int top, final int right,
+ final int bottom) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom);
+ }
+ });
+ }
+ }
+
+ private static final class TvAdServiceCallbackRecord {
+ private final TvAdServiceCallback mCallback;
+ private final Executor mExecutor;
+
+ TvAdServiceCallbackRecord(TvAdServiceCallback callback, Executor executor) {
+ mCallback = callback;
+ mExecutor = executor;
+ }
+
+ public TvAdServiceCallback getCallback() {
+ return mCallback;
+ }
+
+ public void postAdServiceAdded(final String serviceId) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onAdServiceAdded(serviceId);
+ }
+ });
+ }
+
+ public void postAdServiceRemoved(final String serviceId) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onAdServiceRemoved(serviceId);
+ }
+ });
+ }
+
+ public void postAdServiceUpdated(final String serviceId) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onAdServiceUpdated(serviceId);
+ }
+ });
+ }
}
}
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
index 6897a78..6995703 100644
--- a/media/java/android/media/tv/ad/TvAdService.java
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -16,8 +16,37 @@
package android.media.tv.ad;
+import android.annotation.CallSuper;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SuppressLint;
import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.InputChannel;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.internal.os.SomeArgs;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* The TvAdService class represents a TV client-side advertisement service.
@@ -36,9 +65,123 @@
public static final String SERVICE_META_DATA = "android.media.tv.ad.service";
/**
+ * This is the interface name that a service implementing a TV AD service should
+ * say that it supports -- that is, this is the action it uses for its intent filter. To be
+ * supported, the service must also require the
+ * android.Manifest.permission#BIND_TV_AD_SERVICE permission so that other
+ * applications cannot abuse it.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+ public static final String SERVICE_INTERFACE = "android.media.tv.ad.TvAdService";
+
+ private final Handler mServiceHandler = new ServiceHandler();
+ private final RemoteCallbackList<ITvAdServiceCallback> mCallbacks = new RemoteCallbackList<>();
+
+ @Override
+ @Nullable
+ public final IBinder onBind(@NonNull Intent intent) {
+ ITvAdService.Stub tvAdServiceBinder = new ITvAdService.Stub() {
+ @Override
+ public void registerCallback(ITvAdServiceCallback cb) {
+ if (cb != null) {
+ mCallbacks.register(cb);
+ }
+ }
+
+ @Override
+ public void unregisterCallback(ITvAdServiceCallback cb) {
+ if (cb != null) {
+ mCallbacks.unregister(cb);
+ }
+ }
+
+ @Override
+ public void createSession(InputChannel channel, ITvAdSessionCallback cb,
+ String serviceId, String type) {
+ if (cb == null) {
+ return;
+ }
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = channel;
+ args.arg2 = cb;
+ args.arg3 = serviceId;
+ args.arg4 = type;
+ mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args)
+ .sendToTarget();
+ }
+
+ @Override
+ public void sendAppLinkCommand(Bundle command) {
+ onAppLinkCommand(command);
+ }
+ };
+ return tvAdServiceBinder;
+ }
+
+ /**
+ * Called when app link command is received.
+ */
+ public void onAppLinkCommand(@NonNull Bundle command) {
+ }
+
+
+ /**
+ * Returns a concrete implementation of {@link Session}.
+ *
+ * <p>May return {@code null} if this TV AD service fails to create a session for some
+ * reason.
+ *
+ * @param serviceId The ID of the TV AD associated with the session.
+ * @param type The type of the TV AD associated with the session.
+ */
+ @Nullable
+ public abstract Session onCreateSession(@NonNull String serviceId, @NonNull String type);
+
+ /**
* Base class for derived classes to implement to provide a TV AD session.
*/
public abstract static class Session implements KeyEvent.Callback {
+ private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
+
+ private final Object mLock = new Object();
+ // @GuardedBy("mLock")
+ private ITvAdSessionCallback mSessionCallback;
+ // @GuardedBy("mLock")
+ private final List<Runnable> mPendingActions = new ArrayList<>();
+ private final Context mContext;
+ final Handler mHandler;
+ private final WindowManager mWindowManager;
+ private Surface mSurface;
+
+
+ /**
+ * Creates a new Session.
+ *
+ * @param context The context of the application
+ */
+ public Session(@NonNull Context context) {
+ mContext = context;
+ mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ mHandler = new Handler(context.getMainLooper());
+ }
+
+ /**
+ * Releases TvAdService session.
+ */
+ public abstract void onRelease();
+
+ void release() {
+ onRelease();
+ if (mSurface != null) {
+ mSurface.release();
+ mSurface = null;
+ }
+ synchronized (mLock) {
+ mSessionCallback = null;
+ mPendingActions.clear();
+ }
+ }
+
/**
* Starts TvAdService session.
*/
@@ -48,21 +191,264 @@
void startAdService() {
onStartAdService();
}
- }
- /**
- * Implements the internal ITvAdService interface.
- */
- public static class ITvAdSessionWrapper extends ITvAdSession.Stub {
- private final Session mSessionImpl;
-
- public ITvAdSessionWrapper(Session mSessionImpl) {
- this.mSessionImpl = mSessionImpl;
+ @Override
+ public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
+ return false;
}
@Override
- public void startAdService() {
- mSessionImpl.startAdService();
+ public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
+ return false;
}
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int count, @NonNull KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Implement this method to handle touch screen motion events on the current session.
+ *
+ * @param event The motion event being received.
+ * @return If you handled the event, return {@code true}. If you want to allow the event to
+ * be handled by the next receiver, return {@code false}.
+ * @see View#onTouchEvent
+ */
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Implement this method to handle trackball events on the current session.
+ *
+ * @param event The motion event being received.
+ * @return If you handled the event, return {@code true}. If you want to allow the event to
+ * be handled by the next receiver, return {@code false}.
+ * @see View#onTrackballEvent
+ */
+ public boolean onTrackballEvent(@NonNull MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Implement this method to handle generic motion events on the current session.
+ *
+ * @param event The motion event being received.
+ * @return If you handled the event, return {@code true}. If you want to allow the event to
+ * be handled by the next receiver, return {@code false}.
+ * @see View#onGenericMotionEvent
+ */
+ public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
+ return false;
+ }
+
+ /**
+ * Assigns a size and position to the surface passed in {@link #onSetSurface}. The position
+ * is relative to the overlay view that sits on top of this surface.
+ *
+ * @param left Left position in pixels, relative to the overlay view.
+ * @param top Top position in pixels, relative to the overlay view.
+ * @param right Right position in pixels, relative to the overlay view.
+ * @param bottom Bottom position in pixels, relative to the overlay view.
+ */
+ @CallSuper
+ public void layoutSurface(final int left, final int top, final int right,
+ final int bottom) {
+ if (left > right || top > bottom) {
+ throw new IllegalArgumentException("Invalid parameter");
+ }
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top
+ + ", r=" + right + ", b=" + bottom + ",)");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onLayoutSurface(left, top, right, bottom);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in layoutSurface", e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Called when the application sets the surface.
+ *
+ * <p>The TV AD service should render AD UI onto the given surface. When called with
+ * {@code null}, the AD service should immediately free any references to the currently set
+ * surface and stop using it.
+ *
+ * @param surface The surface to be used for AD UI rendering. Can be {@code null}.
+ * @return {@code true} if the surface was set successfully, {@code false} otherwise.
+ */
+ public abstract boolean onSetSurface(@Nullable Surface surface);
+
+ /**
+ * Called after any structural changes (format or size) have been made to the surface passed
+ * in {@link #onSetSurface}. This method is always called at least once, after
+ * {@link #onSetSurface} is called with non-null surface.
+ *
+ * @param format The new {@link PixelFormat} of the surface.
+ * @param width The new width of the surface.
+ * @param height The new height of the surface.
+ */
+ public void onSurfaceChanged(@PixelFormat.Format int format, int width, int height) {
+ }
+
+ /**
+ * Takes care of dispatching incoming input events and tells whether the event was handled.
+ */
+ int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
+ if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")");
+ if (event instanceof KeyEvent) {
+ KeyEvent keyEvent = (KeyEvent) event;
+ if (keyEvent.dispatch(this, mDispatcherState, this)) {
+ return TvAdManager.Session.DISPATCH_HANDLED;
+ }
+
+ // TODO: special handlings of navigation keys and media keys
+ } else if (event instanceof MotionEvent) {
+ MotionEvent motionEvent = (MotionEvent) event;
+ final int source = motionEvent.getSource();
+ if (motionEvent.isTouchEvent()) {
+ if (onTouchEvent(motionEvent)) {
+ return TvAdManager.Session.DISPATCH_HANDLED;
+ }
+ } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+ if (onTrackballEvent(motionEvent)) {
+ return TvAdManager.Session.DISPATCH_HANDLED;
+ }
+ } else {
+ if (onGenericMotionEvent(motionEvent)) {
+ return TvAdManager.Session.DISPATCH_HANDLED;
+ }
+ }
+ }
+ // TODO: handle overlay view
+ return TvAdManager.Session.DISPATCH_NOT_HANDLED;
+ }
+
+
+ private void initialize(ITvAdSessionCallback callback) {
+ synchronized (mLock) {
+ mSessionCallback = callback;
+ for (Runnable runnable : mPendingActions) {
+ runnable.run();
+ }
+ mPendingActions.clear();
+ }
+ }
+
+ /**
+ * Calls {@link #onSetSurface}.
+ */
+ void setSurface(Surface surface) {
+ onSetSurface(surface);
+ if (mSurface != null) {
+ mSurface.release();
+ }
+ mSurface = surface;
+ // TODO: Handle failure.
+ }
+
+ /**
+ * Calls {@link #onSurfaceChanged}.
+ */
+ void dispatchSurfaceChanged(int format, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width
+ + ", height=" + height + ")");
+ }
+ onSurfaceChanged(format, width, height);
+ }
+
+ private void executeOrPostRunnableOnMainThread(Runnable action) {
+ synchronized (mLock) {
+ if (mSessionCallback == null) {
+ // The session is not initialized yet.
+ mPendingActions.add(action);
+ } else {
+ if (mHandler.getLooper().isCurrentThread()) {
+ action.run();
+ } else {
+ // Posts the runnable if this is not called from the main thread
+ mHandler.post(action);
+ }
+ }
+ }
+ }
+ }
+
+
+ @SuppressLint("HandlerLeak")
+ private final class ServiceHandler extends Handler {
+ private static final int DO_CREATE_SESSION = 1;
+ private static final int DO_NOTIFY_SESSION_CREATED = 2;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case DO_CREATE_SESSION: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ InputChannel channel = (InputChannel) args.arg1;
+ ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg2;
+ String serviceId = (String) args.arg3;
+ String type = (String) args.arg4;
+ args.recycle();
+ TvAdService.Session sessionImpl = onCreateSession(serviceId, type);
+ if (sessionImpl == null) {
+ try {
+ // Failed to create a session.
+ cb.onSessionCreated(null);
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in onSessionCreated", e);
+ }
+ return;
+ }
+ ITvAdSession stub =
+ new ITvAdSessionWrapper(TvAdService.this, sessionImpl, channel);
+
+ SomeArgs someArgs = SomeArgs.obtain();
+ someArgs.arg1 = sessionImpl;
+ someArgs.arg2 = stub;
+ someArgs.arg3 = cb;
+ mServiceHandler.obtainMessage(
+ DO_NOTIFY_SESSION_CREATED, someArgs).sendToTarget();
+ return;
+ }
+ case DO_NOTIFY_SESSION_CREATED: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ Session sessionImpl = (Session) args.arg1;
+ ITvAdSession stub = (ITvAdSession) args.arg2;
+ ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg3;
+ try {
+ cb.onSessionCreated(stub);
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in onSessionCreated", e);
+ }
+ if (sessionImpl != null) {
+ sessionImpl.initialize(cb);
+ }
+ args.recycle();
+ return;
+ }
+ default: {
+ Log.w(TAG, "Unhandled message code: " + msg.what);
+ return;
+ }
+ }
+ }
+
}
}
diff --git a/media/java/android/media/tv/ad/TvAdServiceInfo.java b/media/java/android/media/tv/ad/TvAdServiceInfo.java
index ed04f1f..45dc89d 100644
--- a/media/java/android/media/tv/ad/TvAdServiceInfo.java
+++ b/media/java/android/media/tv/ad/TvAdServiceInfo.java
@@ -24,6 +24,7 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.os.Parcel;
import android.os.Parcelable;
@@ -63,8 +64,7 @@
if (context == null) {
throw new IllegalArgumentException("context cannot be null.");
}
- // TODO: use a constant
- Intent intent = new Intent("android.media.tv.ad.TvAdService").setComponent(component);
+ Intent intent = new Intent(TvAdService.SERVICE_INTERFACE).setComponent(component);
ResolveInfo resolveInfo = context.getPackageManager().resolveService(
intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
if (resolveInfo == null) {
@@ -80,6 +80,7 @@
mService = resolveInfo;
mId = id;
+ mTypes.addAll(types);
}
private TvAdServiceInfo(ResolveInfo service, String id, List<String> types) {
@@ -147,9 +148,8 @@
ResolveInfo resolveInfo, Context context, List<String> types) {
ServiceInfo serviceInfo = resolveInfo.serviceInfo;
PackageManager pm = context.getPackageManager();
- // TODO: use constant for the metadata
try (XmlResourceParser parser =
- serviceInfo.loadXmlMetaData(pm, "android.media.tv.ad.service")) {
+ serviceInfo.loadXmlMetaData(pm, TvAdService.SERVICE_META_DATA)) {
if (parser == null) {
throw new IllegalStateException(
"No " + "android.media.tv.ad.service"
@@ -171,7 +171,15 @@
+ XML_START_TAG_NAME + " tag for " + serviceInfo.name);
}
- // TODO: parse attributes
+ TypedArray sa = resources.obtainAttributes(attrs,
+ com.android.internal.R.styleable.TvAdService);
+ CharSequence[] textArr = sa.getTextArray(
+ com.android.internal.R.styleable.TvAdService_adServiceTypes);
+ for (CharSequence cs : textArr) {
+ types.add(cs.toString().toLowerCase());
+ }
+
+ sa.recycle();
} catch (IOException | XmlPullParserException e) {
throw new IllegalStateException(
"Failed reading meta-data for " + serviceInfo.packageName, e);
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
index 1a3771a..5e67fe9 100644
--- a/media/java/android/media/tv/ad/TvAdView.java
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -16,8 +16,20 @@
package android.media.tv.ad;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.util.AttributeSet;
import android.util.Log;
+import android.util.Xml;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
import android.view.ViewGroup;
/**
@@ -28,18 +40,166 @@
private static final String TAG = "TvAdView";
private static final boolean DEBUG = false;
- // TODO: create session
- private TvAdManager.Session mSession;
+ private final TvAdManager mTvAdManager;
- public TvAdView(Context context) {
- super(context, /* attrs = */null, /* defStyleAttr = */0);
+ private final Handler mHandler = new Handler();
+ private TvAdManager.Session mSession;
+ private MySessionCallback mSessionCallback;
+
+ private final AttributeSet mAttrs;
+ private final int mDefStyleAttr;
+ private final XmlResourceParser mParser;
+
+ private SurfaceView mSurfaceView;
+ private Surface mSurface;
+
+ private boolean mSurfaceChanged;
+ private int mSurfaceFormat;
+ private int mSurfaceWidth;
+ private int mSurfaceHeight;
+
+ private boolean mUseRequestedSurfaceLayout;
+ private int mSurfaceViewLeft;
+ private int mSurfaceViewRight;
+ private int mSurfaceViewTop;
+ private int mSurfaceViewBottom;
+
+
+
+ private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format
+ + ", width=" + width + ", height=" + height + ")");
+ }
+ mSurfaceFormat = format;
+ mSurfaceWidth = width;
+ mSurfaceHeight = height;
+ mSurfaceChanged = true;
+ dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurface = holder.getSurface();
+ setSessionSurface(mSurface);
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ mSurface = null;
+ mSurfaceChanged = false;
+ setSessionSurface(null);
+ }
+ };
+
+
+ public TvAdView(@NonNull Context context) {
+ this(context, null, 0);
+ }
+
+ public TvAdView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TvAdView(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ int sourceResId = Resources.getAttributeSetSourceResId(attrs);
+ if (sourceResId != Resources.ID_NULL) {
+ Log.d(TAG, "Build local AttributeSet");
+ mParser = context.getResources().getXml(sourceResId);
+ mAttrs = Xml.asAttributeSet(mParser);
+ } else {
+ Log.d(TAG, "Use passed in AttributeSet");
+ mParser = null;
+ mAttrs = attrs;
+ }
+ mDefStyleAttr = defStyleAttr;
+ resetSurfaceView();
+ mTvAdManager = (TvAdManager) getContext().getSystemService(Context.TV_AD_SERVICE);
}
@Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (DEBUG) {
- Log.d(TAG,
- "onLayout (left=" + l + ", top=" + t + ", right=" + r + ", bottom=" + b + ",)");
+ Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right
+ + ", bottom=" + bottom + ",)");
+ }
+ if (mUseRequestedSurfaceLayout) {
+ mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight,
+ mSurfaceViewBottom);
+ } else {
+ mSurfaceView.layout(0, 0, right - left, bottom - top);
+ }
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec);
+ int width = mSurfaceView.getMeasuredWidth();
+ int height = mSurfaceView.getMeasuredHeight();
+ int childState = mSurfaceView.getMeasuredState();
+ setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
+ resolveSizeAndState(height, heightMeasureSpec,
+ childState << MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
+ @Override
+ public void onVisibilityChanged(@NonNull View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ mSurfaceView.setVisibility(visibility);
+ }
+
+ private void resetSurfaceView() {
+ if (mSurfaceView != null) {
+ mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback);
+ removeView(mSurfaceView);
+ }
+ mSurface = null;
+ mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) {
+ @Override
+ protected void updateSurface() {
+ super.updateSurface();
+ }};
+ // The surface view's content should be treated as secure all the time.
+ mSurfaceView.setSecure(true);
+ mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
+ mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+
+ mSurfaceView.setZOrderOnTop(false);
+ mSurfaceView.setZOrderMediaOverlay(true);
+
+ addView(mSurfaceView);
+ }
+
+ private void setSessionSurface(Surface surface) {
+ if (mSession == null) {
+ return;
+ }
+ mSession.setSurface(surface);
+ }
+
+ private void dispatchSurfaceChanged(int format, int width, int height) {
+ if (mSession == null) {
+ return;
+ }
+ //mSession.dispatchSurfaceChanged(format, width, height);
+ }
+
+ /**
+ * Prepares the AD service of corresponding {@link TvAdService}.
+ *
+ * @param serviceId the AD service ID, which can be found in TvAdServiceInfo#getId().
+ */
+ public void prepareAdService(@NonNull String serviceId, @NonNull String type) {
+ if (DEBUG) {
+ Log.d(TAG, "prepareAdService");
+ }
+ mSessionCallback = new TvAdView.MySessionCallback(serviceId);
+ if (mTvAdManager != null) {
+ mTvAdManager.createSession(serviceId, type, mSessionCallback, mHandler);
}
}
@@ -54,4 +214,75 @@
mSession.startAdService();
}
}
+
+ private class MySessionCallback extends TvAdManager.SessionCallback {
+ final String mServiceId;
+
+ MySessionCallback(String serviceId) {
+ mServiceId = serviceId;
+ }
+
+ @Override
+ public void onSessionCreated(TvAdManager.Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onSessionCreated()");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onSessionCreated - session already created");
+ // This callback is obsolete.
+ if (session != null) {
+ session.release();
+ }
+ return;
+ }
+ mSession = session;
+ if (session != null) {
+ // mSurface may not be ready yet as soon as starting an application.
+ // In the case, we don't send Session.setSurface(null) unnecessarily.
+ // setSessionSurface will be called in surfaceCreated.
+ if (mSurface != null) {
+ setSessionSurface(mSurface);
+ if (mSurfaceChanged) {
+ dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
+ }
+ }
+ } else {
+ // Failed to create
+ // Todo: forward error to Tv App
+ mSessionCallback = null;
+ }
+ }
+
+ @Override
+ public void onSessionReleased(TvAdManager.Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onSessionReleased()");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onSessionReleased - session not created");
+ return;
+ }
+ mSessionCallback = null;
+ mSession = null;
+ }
+
+ @Override
+ public void onLayoutSurface(
+ TvAdManager.Session session, int left, int top, int right, int bottom) {
+ if (DEBUG) {
+ Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right="
+ + right + ", bottom=" + bottom + ",)");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onLayoutSurface - session not created");
+ return;
+ }
+ mSurfaceViewLeft = left;
+ mSurfaceViewTop = top;
+ mSurfaceViewRight = right;
+ mSurfaceViewBottom = bottom;
+ mUseRequestedSurfaceLayout = true;
+ requestLayout();
+ }
+ }
}
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 7739184..e3dba03 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -48,6 +48,7 @@
void onRequestCurrentChannelLcn(int seq);
void onRequestStreamVolume(int seq);
void onRequestTrackInfoList(int seq);
+ void onRequestSelectedTrackInfo(int seq);
void onRequestCurrentTvInputId(int seq);
void onRequestTimeShiftMode(int seq);
void onRequestAvailableSpeeds(int seq);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index 41cbe4a..4316d05 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -102,6 +102,8 @@
int UserId);
void notifyAdResponse(in IBinder sessionToken, in AdResponse response, int UserId);
void notifyAdBufferConsumed(in IBinder sessionToken, in AdBuffer buffer, int userId);
+ void sendSelectedTrackInfo(in IBinder sessionToken, in List<TvTrackInfo> tracks,
+ int userId);
void createMediaView(in IBinder sessionToken, in IBinder windowToken, in Rect frame,
int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index 052bc3d..ba7cf13 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -78,6 +78,7 @@
void notifyBroadcastInfoResponse(in BroadcastInfoResponse response);
void notifyAdResponse(in AdResponse response);
void notifyAdBufferConsumed(in AdBuffer buffer);
+ void sendSelectedTrackInfo(in List<TvTrackInfo> tracks);
void createMediaView(in IBinder windowToken, in Rect frame);
void relayoutMediaView(in Rect frame);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index 9e43e79..416b8f1 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -50,6 +50,7 @@
void onRequestCurrentTvInputId();
void onRequestTimeShiftMode();
void onRequestAvailableSpeeds();
+ void onRequestSelectedTrackInfo();
void onRequestStartRecording(in String requestId, in Uri programUri);
void onRequestStopRecording(in String recordingId);
void onRequestScheduleRecording(in String requestId, in String inputId, in Uri channelUri,
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index 253ade8..518b08a 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -102,6 +102,7 @@
private static final int DO_NOTIFY_RECORDING_SCHEDULED = 45;
private static final int DO_SEND_TIME_SHIFT_MODE = 46;
private static final int DO_SEND_AVAILABLE_SPEEDS = 47;
+ private static final int DO_SEND_SELECTED_TRACK_INFO = 48;
private final HandlerCaller mCaller;
private Session mSessionImpl;
@@ -247,6 +248,10 @@
args.recycle();
break;
}
+ case DO_SEND_SELECTED_TRACK_INFO: {
+ mSessionImpl.sendSelectedTrackInfo((List<TvTrackInfo>) msg.obj);
+ break;
+ }
case DO_NOTIFY_VIDEO_AVAILABLE: {
mSessionImpl.notifyVideoAvailable();
break;
@@ -526,6 +531,12 @@
}
@Override
+ public void sendSelectedTrackInfo(List<TvTrackInfo> tracks) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageO(DO_SEND_SELECTED_TRACK_INFO, tracks));
+ }
+
+ @Override
public void notifyTracksChanged(List<TvTrackInfo> tracks) {
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_TRACKS_CHANGED, tracks));
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index 7cce84a..bf4379f 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -33,7 +33,6 @@
import android.media.tv.TvInputManager;
import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
-import android.media.tv.interactive.TvInteractiveAppService.Session;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -506,6 +505,18 @@
}
@Override
+ public void onRequestSelectedTrackInfo(int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestSelectedTrackInfo();
+ }
+ }
+
+ @Override
public void onRequestCurrentTvInputId(int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -1209,6 +1220,18 @@
}
}
+ void sendSelectedTrackInfo(@NonNull List<TvTrackInfo> tracks) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendSelectedTrackInfo(mToken, tracks, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
void sendCurrentTvInputId(@Nullable String inputId) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
@@ -2108,6 +2131,15 @@
});
}
+ void postRequestSelectedTrackInfo() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestSelectedTrackInfo(mSession);
+ }
+ });
+ }
+
void postRequestCurrentTvInputId() {
mHandler.post(new Runnable() {
@Override
@@ -2378,6 +2410,15 @@
}
/**
+ * This is called when {@link TvInteractiveAppService.Session#requestSelectedTrackInfo()} is
+ * called.
+ *
+ * @param session A {@link TvInteractiveAppManager.Session} associated with this callback.
+ */
+ public void onRequestSelectedTrackInfo(Session session) {
+ }
+
+ /**
* This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId} is
* called.
*
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 2419404..5cc86ba 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -932,6 +932,16 @@
@NonNull Bundle data) {
}
+ /**
+ * Called when the TV App sends the selected track info as a response to
+ * requestSelectedTrackInfo.
+ *
+ * @param tracks
+ * @hide
+ */
+ public void onSelectedTrackInfo(List<TvTrackInfo> tracks) {
+ }
+
@Override
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
return false;
@@ -1338,6 +1348,30 @@
}
/**
+ * Requests the currently selected {@link TvTrackInfo} from the TV App.
+ *
+ * <p> Normally, track info cannot be synchronized until the channel has
+ * been changed. This is used when the session of the TIAS is newly
+ * created and the normal synchronization has not happened yet.
+ * @hide
+ */
+ @CallSuper
+ public void requestSelectedTrackInfo() {
+ executeOrPostRunnableOnMainThread(() -> {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestSelectedTrackInfo");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestSelectedTrackInfo();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestSelectedTrackInfo", e);
+ }
+ });
+ }
+
+ /**
* Requests starting of recording
*
* <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to
@@ -1781,6 +1815,13 @@
onTvMessage(type, data);
}
+ void sendSelectedTrackInfo(List<TvTrackInfo> tracks) {
+ if (DEBUG) {
+ Log.d(TAG, "notifySelectedTrackInfo (tracks= " + tracks + ")");
+ }
+ onSelectedTrackInfo(tracks);
+ }
+
/**
* Calls {@link #onAdBufferConsumed}.
*/
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index cbaf5e4..40a12e4 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -582,6 +582,20 @@
}
/**
+ * Sends the currently selected track info to the TV Interactive App.
+ *
+ * @hide
+ */
+ public void sendSelectedTrackInfo(@Nullable List<TvTrackInfo> tracks) {
+ if (DEBUG) {
+ Log.d(TAG, "sendSelectedTrackInfo");
+ }
+ if (mSession != null) {
+ mSession.sendSelectedTrackInfo(tracks);
+ }
+ }
+
+ /**
* Sends current TV input ID to related TV interactive app.
*
* @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is
@@ -1197,6 +1211,16 @@
}
/**
+ * This is called when {@link TvInteractiveAppService.Session#requestSelectedTrackInfo()} is
+ * called.
+ *
+ * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+ * @hide
+ */
+ public void onRequestSelectedTrackInfo(@NonNull String iAppServiceId) {
+ }
+
+ /**
* This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId()} is
* called.
*
@@ -1714,6 +1738,28 @@
}
@Override
+ public void onRequestSelectedTrackInfo(Session session) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestSelectedTrackInfo");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestSelectedTrackInfo - session not created");
+ return;
+ }
+ synchronized (mCallbackLock) {
+ if (mCallbackExecutor != null) {
+ mCallbackExecutor.execute(() -> {
+ synchronized (mCallbackLock) {
+ if (mCallback != null) {
+ mCallback.onRequestSelectedTrackInfo(mIAppServiceId);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ @Override
public void onRequestCurrentTvInputId(Session session) {
if (DEBUG) {
Log.d(TAG, "onRequestCurrentTvInputId");
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index 3fcb871..00b0e57 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -967,7 +967,7 @@
ScopedLocalRef<jobject> filter(env);
{
android::Mutex::Autolock autoLock(mLock);
- if (env->IsSameObject(filter.get(), nullptr)) {
+ if (env->IsSameObject(mFilterObj, nullptr)) {
ALOGE("FilterClientCallbackImpl::onFilterStatus:"
"Filter object has been freed. Ignoring callback.");
return;
diff --git a/nfc/Android.bp b/nfc/Android.bp
index 5d1404a..74bec3e 100644
--- a/nfc/Android.bp
+++ b/nfc/Android.bp
@@ -69,7 +69,12 @@
jarjar_rules: ":nfc-jarjar-rules",
lint: {
strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
},
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.nfcservices",
+ ],
}
filegroup {
diff --git a/nfc/lint-baseline.xml b/nfc/lint-baseline.xml
new file mode 100644
index 0000000..1dfdd29
--- /dev/null
+++ b/nfc/lint-baseline.xml
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `new android.nfc.cardemulation.AidGroup`"
+ errorLine1=" AidGroup aidGroup = new AidGroup(aids, category);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="377"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.AidGroup#getAids`"
+ errorLine1=" return (group != null ? group.getAids() : null);"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="537"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.AidGroup#getAids`"
+ errorLine1=" return (group != null ? group.getAids() : null);"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="547"
+ column="47"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getAids`"
+ errorLine1=" return (serviceInfo != null ? serviceInfo.getAids() : null);"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="714"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getAids`"
+ errorLine1=" return (serviceInfo != null ? serviceInfo.getAids() : null);"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="724"
+ column="59"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#isOnHost`"
+ errorLine1=" if (!serviceInfo.isOnHost()) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="755"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`"
+ errorLine1=" return serviceInfo.getOffHostSecureElement() == null ?"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="756"
+ column="40"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`"
+ errorLine1=' "OffHost" : serviceInfo.getOffHostSecureElement();'
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="757"
+ column="53"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#isOnHost`"
+ errorLine1=" if (!serviceInfo.isOnHost()) {"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="772"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`"
+ errorLine1=" return serviceInfo.getOffHostSecureElement() == null ?"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="773"
+ column="44"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`"
+ errorLine1=' "Offhost" : serviceInfo.getOffHostSecureElement();'
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="774"
+ column="57"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getDescription`"
+ errorLine1=" return (serviceInfo != null ? serviceInfo.getDescription() : null);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="798"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getDescription`"
+ errorLine1=" return (serviceInfo != null ? serviceInfo.getDescription() : null);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="808"
+ column="59"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`"
+ errorLine1=" if (!activity.isResumed()) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="1032"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`"
+ errorLine1=" if (!activity.isResumed()) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java"
+ line="1066"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`"
+ errorLine1=" resumed = activity.isResumed();"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/NfcActivityManager.java"
+ line="124"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`"
+ errorLine1=" if (!activity.isResumed()) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/NfcAdapter.java"
+ line="2457"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`"
+ errorLine1=" if (!activity.isResumed()) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/NfcFCardEmulation.java"
+ line="315"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`"
+ errorLine1=" if (!activity.isResumed()) {"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="frameworks/base/nfc/java/android/nfc/cardemulation/NfcFCardEmulation.java"
+ line="351"
+ column="23"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
index b5af845..9af799c 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
@@ -31,7 +31,7 @@
import android.os.Process;
import android.util.Log;
-import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
import java.io.IOException;
import java.util.Arrays;
@@ -105,7 +105,7 @@
}
}
- @Nullable
+ @NonNull
private String[] getRequestedPermissions(String callingPackage) {
String[] requestedPermissions = null;
try {
@@ -115,7 +115,7 @@
// Should be unreachable because we've just fetched the packageName above.
Log.e(TAG, "Package not found for " + callingPackage);
}
- return requestedPermissions;
+ return requestedPermissions == null ? new String[]{} : requestedPermissions;
}
void startUnarchive() {
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index 965fdcf..ed90284 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -73,6 +73,11 @@
android:authorities="com.android.spa.gallery.debug.provider"
android:exported="false">
</provider>
-
+ <activity
+ android:name="com.android.settingslib.spa.gallery.SpaDialogActivity"
+ android:excludeFromRecents="true"
+ android:exported="true"
+ android:theme="@style/Theme.SpaLib.Dialog">
+ </activity>
</application>
</manifest>
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt
new file mode 100644
index 0000000..8b80fe2
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2024 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.gallery
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.WarningAmber
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import com.android.settingslib.spa.framework.common.LogCategory
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.dialog.getDialogWidth
+
+
+class SpaDialogActivity : ComponentActivity() {
+ private val spaEnvironment get() = SpaEnvironmentFactory.instance
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK)
+ setContent {
+ SettingsTheme {
+ Content()
+ }
+ }
+ }
+
+ @Composable
+ fun Content() {
+ var openAlertDialog by remember { mutableStateOf(false) }
+ AlertDialog(openAlertDialog)
+ LaunchedEffect(key1 = Unit) {
+ openAlertDialog = true
+ }
+ }
+
+ @Composable
+ fun AlertDialog(openAlertDialog: Boolean) {
+ when {
+ openAlertDialog -> {
+ AlertDialogExample(
+ onDismissRequest = { finish() },
+ onConfirmation = { finish() },
+ dialogTitle = intent.getStringExtra(DIALOG_TITLE) ?: DIALOG_TITLE,
+ dialogText = intent.getStringExtra(DIALOG_TEXT) ?: DIALOG_TEXT,
+ icon = Icons.Default.WarningAmber
+ )
+ }
+ }
+ }
+
+ @Composable
+ fun AlertDialogExample(
+ onDismissRequest: () -> Unit,
+ onConfirmation: () -> Unit,
+ dialogTitle: String,
+ dialogText: String,
+ icon: ImageVector,
+ ) {
+ AlertDialog(
+ modifier = Modifier.width(getDialogWidth()),
+ icon = {
+ Icon(icon, contentDescription = null)
+ },
+ title = {
+ Text(text = dialogTitle)
+ },
+ text = {
+ Text(text = dialogText)
+ },
+ onDismissRequest = {
+ onDismissRequest()
+ },
+ dismissButton = {
+ OutlinedButton(
+ onClick = {
+ onDismissRequest()
+ }
+ ) {
+ Text(intent.getStringExtra(DISMISS_TEXT) ?: DISMISS_TEXT)
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ onConfirmation()
+ },
+ ) {
+ Text(intent.getStringExtra(CONFIRM_TEXT) ?: CONFIRM_TEXT)
+ }
+ }
+ )
+ }
+
+ companion object {
+ private const val TAG = "SpaDialogActivity"
+ private const val DIALOG_TITLE = "dialogTitle"
+ private const val DIALOG_TEXT = "dialogText"
+ private const val CONFIRM_TEXT = "confirmText"
+ private const val DISMISS_TEXT = "dismissText"
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml
index 25846ec..4b5a9bc 100644
--- a/packages/SettingsLib/Spa/spa/res/values/themes.xml
+++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml
@@ -22,4 +22,6 @@
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
+
+ <style name="Theme.SpaLib.Dialog" parent="Theme.Material3.DayNight.Dialog"/>
</resources>
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
index 207c174..8ffd799 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
@@ -99,7 +99,7 @@
}
@Composable
-private fun getDialogWidth(): Dp {
+fun getDialogWidth(): Dp {
val configuration = LocalConfiguration.current
return configuration.screenWidthDp.dp * when (configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> 0.6f
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
index 1c830c1..74b556e 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
@@ -162,6 +162,7 @@
uid = checkNotNull(applicationInfo).uid,
packageName = packageName) })
RestrictedSwitchPreference(switchModel, restrictions, restrictionsProviderFactory)
+ InfoPageAdditionalContent(record, isAllowed)
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
index 916d83a..3f7a852 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
@@ -77,6 +77,9 @@
* Sets whether the permission is allowed for the given app.
*/
fun setAllowed(record: T, newAllowed: Boolean)
+
+ @Composable
+ fun InfoPageAdditionalContent(record: T, isAllowed: () -> Boolean?){}
}
interface TogglePermissionAppListProvider {
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 5aa2bfc..a4b3af9 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -338,6 +338,9 @@
<!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=5165842622743212268] -->
<string name="bluetooth_talkback_input_peripheral">Input Peripheral</string>
+ <!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=26580326066627664] -->
+ <string name="bluetooth_talkback_hearing_aids">Hearing Aids</string>
+
<!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=5615463912185280812] -->
<string name="bluetooth_talkback_bluetooth">Bluetooth</string>
@@ -1079,6 +1082,10 @@
<!-- [CHAR_LIMIT=NONE] Label for battery on main page of settings -->
<string name="power_remaining_settings_home_page"><xliff:g id="percentage" example="10%">%1$s</xliff:g> - <xliff:g id="time_string" example="1 hour left based on your usage">%2$s</xliff:g></string>
+ <!-- [CHAR_LIMIT=NONE] Label for charging on hold on main page of settings -->
+ <string name="power_charging_on_hold_settings_home_page"><xliff:g id="level">%1$s</xliff:g> - Charging on hold to protect battery</string>
+ <!-- [CHAR_LIMIT=NONE] Label for incompatible charging accessory on main page of settings -->
+ <string name="power_incompatible_charging_settings_home_page"><xliff:g id="level">%1$s</xliff:g> - Checking charging accessory</string>
<!-- [CHAR_LIMIT=40] Label for estimated remaining duration of battery discharging -->
<string name="power_remaining_duration_only">About <xliff:g id="time_remaining">%1$s</xliff:g> left</string>
<!-- [CHAR_LIMIT=40] Label for battery level chart when discharging with duration -->
@@ -1139,11 +1146,13 @@
<!-- Battery Info screen. Value for a status item. Used for diagnostic info screens, precise translation isn't needed -->
<string name="battery_info_status_discharging">Not charging</string>
<!-- Battery Info screen. Value for a status item. A state which device is connected with any charger(e.g. USB, Adapter or Wireless) but not charging yet. Used for diagnostic info screens, precise translation isn't needed -->
- <string name="battery_info_status_not_charging">Connected, not charging</string>
+ <string name="battery_info_status_not_charging">Connected, but not charging</string>
<!-- Battery Info screen. Value for a status item. Used for diagnostic info screens, precise translation isn't needed -->
<string name="battery_info_status_full">Charged</string>
<!-- [CHAR_LIMIT=40] Battery Info screen. Value for a status item. A state which device is fully charged -->
<string name="battery_info_status_full_charged">Fully Charged</string>
+ <!-- [CHAR_LIMIT=None] Battery Info screen. Value for a status item. A state which device charging on hold -->
+ <string name="battery_info_status_charging_on_hold">Charging on hold</string>
<!-- Summary for settings preference disabled by administrator [CHAR LIMIT=50] -->
<string name="disabled_by_admin_summary_text">Controlled by admin</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index c2be571..fb14a17 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -1,6 +1,7 @@
package com.android.settingslib;
import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_USER_LABEL;
+import static android.webkit.Flags.updateServiceV2;
import android.annotation.ColorInt;
import android.app.admin.DevicePolicyManager;
@@ -34,6 +35,7 @@
import android.net.wifi.WifiInfo;
import android.os.BatteryManager;
import android.os.Build;
+import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
@@ -44,6 +46,9 @@
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.util.Log;
+import android.webkit.IWebViewUpdateService;
+import android.webkit.WebViewFactory;
+import android.webkit.WebViewProviderInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -65,6 +70,8 @@
public class Utils {
+ private static final String TAG = "Utils";
+
@VisibleForTesting
static final String STORAGE_MANAGER_ENABLED_PROPERTY =
"ro.storage_manager.enabled";
@@ -76,6 +83,7 @@
private static String sPermissionControllerPackageName;
private static String sServicesSystemSharedLibPackageName;
private static String sSharedSystemSharedLibPackageName;
+ private static String sDefaultWebViewPackageName;
static final int[] WIFI_PIE = {
com.android.internal.R.drawable.ic_wifi_signal_0,
@@ -445,6 +453,7 @@
|| packageName.equals(sServicesSystemSharedLibPackageName)
|| packageName.equals(sSharedSystemSharedLibPackageName)
|| packageName.equals(PrintManager.PRINT_SPOOLER_PACKAGE_NAME)
+ || (updateServiceV2() && packageName.equals(getDefaultWebViewPackageName()))
|| isDeviceProvisioningPackage(resources, packageName);
}
@@ -459,6 +468,29 @@
}
/**
+ * Fetch the package name of the default WebView provider.
+ */
+ @Nullable
+ private static String getDefaultWebViewPackageName() {
+ if (sDefaultWebViewPackageName != null) {
+ return sDefaultWebViewPackageName;
+ }
+
+ try {
+ IWebViewUpdateService service = WebViewFactory.getUpdateService();
+ if (service != null) {
+ WebViewProviderInfo provider = service.getDefaultWebViewPackage();
+ if (provider != null) {
+ sDefaultWebViewPackageName = provider.packageName;
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException when trying to fetch default WebView package Name", e);
+ }
+ return sDefaultWebViewPackageName;
+ }
+
+ /**
* Returns the Wifi icon resource for a given RSSI level.
*
* @param level The number of bars to show (0-4)
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 0ffcc45..f7f0673 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -117,6 +117,12 @@
}
}
+ if (cachedDevice.isHearingAidDevice()) {
+ return new Pair<>(getBluetoothDrawable(context,
+ com.android.internal.R.drawable.ic_bt_hearing_aid),
+ context.getString(R.string.bluetooth_talkback_hearing_aids));
+ }
+
List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles();
int resId = 0;
for (LocalBluetoothProfile profile : profiles) {
@@ -125,7 +131,8 @@
// The device should show hearing aid icon if it contains any hearing aid related
// profiles
if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
- return new Pair<>(getBluetoothDrawable(context, profileResId), null);
+ return new Pair<>(getBluetoothDrawable(context, profileResId),
+ context.getString(R.string.bluetooth_talkback_hearing_aids));
}
if (resId == 0) {
resId = profileResId;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 245fe6e..560bc46 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -410,8 +410,13 @@
connectDevice();
}
- void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
+ public void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
mHearingAidInfo = hearingAidInfo;
+ dispatchAttributesChanged();
+ }
+
+ public HearingAidInfo getHearingAidInfo() {
+ return mHearingAidInfo;
}
/**
@@ -1788,4 +1793,40 @@
boolean getUnpairing() {
return mUnpairing;
}
+
+ ListenableFuture<Void> syncProfileForMemberDevice() {
+ return ThreadUtils.getBackgroundExecutor()
+ .submit(
+ () -> {
+ List<Pair<LocalBluetoothProfile, Boolean>> toSync =
+ Stream.of(
+ mProfileManager.getA2dpProfile(),
+ mProfileManager.getHeadsetProfile(),
+ mProfileManager.getHearingAidProfile(),
+ mProfileManager.getLeAudioProfile(),
+ mProfileManager.getLeAudioBroadcastAssistantProfile())
+ .filter(Objects::nonNull)
+ .map(profile -> new Pair<>(profile, profile.isEnabled(mDevice)))
+ .toList();
+
+ for (var t : toSync) {
+ LocalBluetoothProfile profile = t.first;
+ boolean enabledForMain = t.second;
+
+ for (var member : mMemberDevices) {
+ BluetoothDevice btDevice = member.getDevice();
+
+ if (enabledForMain != profile.isEnabled(btDevice)) {
+ Log.d(TAG, "Syncing profile " + profile + " to "
+ + enabledForMain + " for member device "
+ + btDevice.getAnonymizedAddress() + " of main device "
+ + mDevice.getAnonymizedAddress());
+ profile.setEnabled(btDevice, enabledForMain);
+ }
+ }
+ }
+ return null;
+ }
+ );
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index a6536a8c..89fe268 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -349,6 +349,7 @@
if (profileId == BluetoothProfile.HEADSET
|| profileId == BluetoothProfile.A2DP
|| profileId == BluetoothProfile.LE_AUDIO
+ || profileId == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
|| profileId == BluetoothProfile.CSIP_SET_COORDINATOR) {
return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
state);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index a49314a..e67ec48 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -379,6 +379,7 @@
if (hasChanged) {
log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
+ mCachedDevices);
+ preferredMainDevice.syncProfileForMemberDevice();
}
return hasChanged;
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 7409eea..f7ec80b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -16,6 +16,7 @@
package com.android.settingslib.bluetooth;
import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -87,6 +88,14 @@
}
@Test
+ public void getBtClassDrawableWithDescription_typeHearingAid_returnHearingAidDrawable() {
+ when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true);
+ BluetoothUtils.getBtClassDrawableWithDescription(mContext, mCachedBluetoothDevice);
+
+ verify(mContext).getDrawable(com.android.internal.R.drawable.ic_bt_hearing_aid);
+ }
+
+ @Test
public void getBtRainbowDrawableWithDescription_normalHeadset_returnAdaptiveIcon() {
when(mBluetoothDevice.getMetadata(
BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn("false".getBytes());
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index ed545ab..9db8b47 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -18,7 +18,9 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -56,6 +58,8 @@
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
+import java.util.concurrent.ExecutionException;
+
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowBluetoothAdapter.class})
public class CachedBluetoothDeviceTest {
@@ -1815,6 +1819,52 @@
assertThat(mCachedDevice.isConnectedHearingAidDevice()).isFalse();
}
+ @Test
+ public void syncProfileForMemberDevice_hasDiff_shouldSync()
+ throws ExecutionException, InterruptedException {
+ mCachedDevice.addMemberDevice(mSubCachedDevice);
+ when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
+ when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+ when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
+
+ when(mA2dpProfile.isEnabled(mDevice)).thenReturn(true);
+ when(mHearingAidProfile.isEnabled(mDevice)).thenReturn(true);
+ when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+
+ when(mA2dpProfile.isEnabled(mSubDevice)).thenReturn(true);
+ when(mHearingAidProfile.isEnabled(mSubDevice)).thenReturn(false);
+ when(mLeAudioProfile.isEnabled(mSubDevice)).thenReturn(false);
+
+ mCachedDevice.syncProfileForMemberDevice().get();
+
+ verify(mA2dpProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+ verify(mHearingAidProfile).setEnabled(any(BluetoothDevice.class), eq(true));
+ verify(mLeAudioProfile).setEnabled(any(BluetoothDevice.class), eq(true));
+ }
+
+ @Test
+ public void syncProfileForMemberDevice_noDiff_shouldNotSync()
+ throws ExecutionException, InterruptedException {
+ mCachedDevice.addMemberDevice(mSubCachedDevice);
+ when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
+ when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+ when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
+
+ when(mA2dpProfile.isEnabled(mDevice)).thenReturn(false);
+ when(mHearingAidProfile.isEnabled(mDevice)).thenReturn(false);
+ when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+
+ when(mA2dpProfile.isEnabled(mSubDevice)).thenReturn(false);
+ when(mHearingAidProfile.isEnabled(mSubDevice)).thenReturn(false);
+ when(mLeAudioProfile.isEnabled(mSubDevice)).thenReturn(true);
+
+ mCachedDevice.syncProfileForMemberDevice().get();
+
+ verify(mA2dpProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+ verify(mHearingAidProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+ verify(mLeAudioProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+ }
+
private HearingAidInfo getLeftAshaHearingAidInfo() {
return new HearingAidInfo.Builder()
.setAshaDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT)
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index b0abf92..2d442f4 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -1349,6 +1349,26 @@
final int nameCount = names.size();
HashMap<String, String> flagsToValues = new HashMap<>(names.size());
+ if (Flags.loadAconfigDefaults()) {
+ Map<String, Map<String, String>> allDefaults =
+ settingsState.getAconfigDefaultValues();
+
+ if (allDefaults != null) {
+ if (prefix != null) {
+ String namespace = prefix.substring(0, prefix.length() - 1);
+
+ Map<String, String> namespaceDefaults = allDefaults.get(namespace);
+ if (namespaceDefaults != null) {
+ flagsToValues.putAll(namespaceDefaults);
+ }
+ } else {
+ for (Map<String, String> namespaceDefaults : allDefaults.values()) {
+ flagsToValues.putAll(namespaceDefaults);
+ }
+ }
+ }
+ }
+
for (int i = 0; i < nameCount; i++) {
String name = names.get(i);
Setting setting = settingsState.getSettingLocked(name);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 73c2e22..6f3c88f 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -69,6 +69,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -236,6 +237,10 @@
@GuardedBy("mLock")
private int mNextHistoricalOpIdx;
+ @GuardedBy("mLock")
+ @Nullable
+ private Map<String, Map<String, String>> mNamespaceDefaults;
+
public static final int SETTINGS_TYPE_GLOBAL = 0;
public static final int SETTINGS_TYPE_SYSTEM = 1;
public static final int SETTINGS_TYPE_SECURE = 2;
@@ -331,25 +336,21 @@
readStateSyncLocked();
if (Flags.loadAconfigDefaults()) {
- // Only load aconfig defaults if this is the first boot, the XML
- // file doesn't exist yet, or this device is on its first boot after
- // an OTA.
- boolean shouldLoadAconfigValues = isConfigSettingsKey(mKey)
- && (!file.exists()
- || mContext.getPackageManager().isDeviceUpgrading());
- if (shouldLoadAconfigValues) {
+ if (isConfigSettingsKey(mKey)) {
loadAconfigDefaultValuesLocked();
}
}
+
}
}
@GuardedBy("mLock")
private void loadAconfigDefaultValuesLocked() {
+ mNamespaceDefaults = new HashMap<>();
+
for (String fileName : sAconfigTextProtoFilesOnDevice) {
try (FileInputStream inputStream = new FileInputStream(fileName)) {
- byte[] contents = inputStream.readAllBytes();
- loadAconfigDefaultValues(contents);
+ loadAconfigDefaultValues(inputStream.readAllBytes(), mNamespaceDefaults);
} catch (IOException e) {
Slog.e(LOG_TAG, "failed to read protobuf", e);
}
@@ -358,27 +359,21 @@
@VisibleForTesting
@GuardedBy("mLock")
- public void loadAconfigDefaultValues(byte[] fileContents) {
+ public static void loadAconfigDefaultValues(byte[] fileContents,
+ @NonNull Map<String, Map<String, String>> defaultMap) {
try {
- parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents);
-
- if (parsedFlags == null) {
- Slog.e(LOG_TAG, "failed to parse aconfig protobuf");
- return;
- }
-
+ parsed_flags parsedFlags =
+ parsed_flags.parseFrom(fileContents);
for (parsed_flag flag : parsedFlags.getParsedFlagList()) {
- String flagName = flag.getNamespace() + "/"
- + flag.getPackage() + "." + flag.getName();
- String value = flag.getState() == flag_state.ENABLED ? "true" : "false";
-
- Setting existingSetting = getSettingLocked(flagName);
- boolean isDefaultLoaded = existingSetting.getTag() != null
- && existingSetting.getTag().equals(BOOT_LOADED_DEFAULT_TAG);
- if (existingSetting.getValue() == null || isDefaultLoaded) {
- insertSettingLocked(flagName, value, BOOT_LOADED_DEFAULT_TAG,
- false, flag.getPackage());
+ if (!defaultMap.containsKey(flag.getNamespace())) {
+ Map<String, String> defaults = new HashMap<>();
+ defaultMap.put(flag.getNamespace(), defaults);
}
+ String flagName = flag.getNamespace()
+ + "/" + flag.getPackage() + "." + flag.getName();
+ String flagValue = flag.getState() == flag_state.ENABLED
+ ? "true" : "false";
+ defaultMap.get(flag.getNamespace()).put(flagName, flagValue);
}
} catch (IOException e) {
Slog.e(LOG_TAG, "failed to parse protobuf", e);
@@ -443,6 +438,13 @@
return names;
}
+ @Nullable
+ public Map<String, Map<String, String>> getAconfigDefaultValues() {
+ synchronized (mLock) {
+ return mNamespaceDefaults;
+ }
+ }
+
// The settings provider must hold its lock when calling here.
public Setting getSettingLocked(String name) {
if (TextUtils.isEmpty(name)) {
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 24625ea..e55bbec 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -30,6 +30,8 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintStream;
+import java.util.HashMap;
+import java.util.Map;
public class SettingsStateTest extends AndroidTestCase {
public static final String CRAZY_STRING =
@@ -93,7 +95,6 @@
SettingsState settingsState = new SettingsState(
getContext(), lock, mSettingsFile, configKey,
SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
-
parsed_flags flags = parsed_flags
.newBuilder()
.addParsedFlag(parsed_flag
@@ -117,18 +118,13 @@
.build();
synchronized (lock) {
- settingsState.loadAconfigDefaultValues(flags.toByteArray());
- settingsState.persistSettingsLocked();
- }
- settingsState.waitForHandler();
+ Map<String, Map<String, String>> defaults = new HashMap<>();
+ settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults);
+ Map<String, String> namespaceDefaults = defaults.get("test_namespace");
+ assertEquals(2, namespaceDefaults.keySet().size());
- synchronized (lock) {
- assertEquals("false",
- settingsState.getSettingLocked(
- "test_namespace/com.android.flags.flag1").getValue());
- assertEquals("true",
- settingsState.getSettingLocked(
- "test_namespace/com.android.flags.flag2").getValue());
+ assertEquals("false", namespaceDefaults.get("test_namespace/com.android.flags.flag1"));
+ assertEquals("true", namespaceDefaults.get("test_namespace/com.android.flags.flag2"));
}
}
@@ -150,21 +146,18 @@
.build();
synchronized (lock) {
- settingsState.loadAconfigDefaultValues(flags.toByteArray());
- settingsState.persistSettingsLocked();
- }
- settingsState.waitForHandler();
+ Map<String, Map<String, String>> defaults = new HashMap<>();
+ settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults);
- synchronized (lock) {
- assertEquals(null,
- settingsState.getSettingLocked(
- "test_namespace/com.android.flags.flag1").getValue());
+ Map<String, String> namespaceDefaults = defaults.get("test_namespace");
+ assertEquals(null, namespaceDefaults);
}
}
public void testInvalidAconfigProtoDoesNotCrash() {
+ Map<String, Map<String, String>> defaults = new HashMap<>();
SettingsState settingsState = getSettingStateObject();
- settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes());
+ settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes(), defaults);
}
public void testIsBinary() {
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index f7b1a26..7ba889b 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -36,3 +36,10 @@
description: "Animates the floating menu's transition between curved and jagged edges."
bug: "281140482"
}
+
+flag {
+ name: "create_windowless_window_magnifier"
+ namespace: "accessibility"
+ description: "Uses SurfaceControlViewHost to create the magnifier for window magnification."
+ bug: "280992417"
+}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index c23a49c..b464498 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -343,3 +343,10 @@
description: "Relocate Smartspace to bottom of the Lock Screen"
bug: "316212788"
}
+
+flag {
+ name: "pin_input_field_styled_focus_state"
+ namespace: "systemui"
+ description: "Enables styled focus states on pin input field if keyboard is connected"
+ bug: "316106516"
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 91a4d2e..c8e18d7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -20,6 +20,7 @@
import android.os.Bundle
import android.util.SizeF
import android.widget.FrameLayout
+import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@@ -38,6 +39,7 @@
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -58,17 +60,22 @@
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
@@ -86,6 +93,9 @@
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
+import com.android.systemui.communal.ui.compose.extensions.allowGestures
+import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
+import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.res.R
@@ -104,22 +114,59 @@
var toolbarSize: IntSize? by remember { mutableStateOf(null) }
var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
var isDraggingToRemove by remember { mutableStateOf(false) }
+ val gridState = rememberLazyGridState()
+ val contentListState = rememberContentListState(communalContent, viewModel)
+ val reorderingWidgets by viewModel.reorderingWidgets.collectAsState()
+ val selectedIndex = viewModel.selectedIndex.collectAsState()
+ val removeButtonEnabled by remember {
+ derivedStateOf { selectedIndex.value != null || reorderingWidgets }
+ }
+
+ val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize)
+ val contentOffset = beforeContentPadding(contentPadding).toOffset()
Box(
modifier =
- modifier.fillMaxSize().background(LocalAndroidColorScheme.current.outlineVariant),
+ modifier
+ .fillMaxSize()
+ .background(LocalAndroidColorScheme.current.outlineVariant)
+ .pointerInput(gridState, contentOffset, contentListState) {
+ // If not in edit mode, don't allow selecting items.
+ if (!viewModel.isEditMode) return@pointerInput
+ observeTapsWithoutConsuming { offset ->
+ val adjustedOffset = offset - contentOffset
+ val index =
+ gridState.layoutInfo.visibleItemsInfo
+ .firstItemAtOffset(adjustedOffset)
+ ?.index
+ val newIndex =
+ if (index?.let(contentListState::isItemEditable) == true) {
+ index
+ } else {
+ null
+ }
+ viewModel.setSelectedIndex(newIndex)
+ }
+ },
) {
CommunalHubLazyGrid(
communalContent = communalContent,
viewModel = viewModel,
- contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize),
+ contentPadding = contentPadding,
+ contentOffset = contentOffset,
setGridCoordinates = { gridCoordinates = it },
- updateDragPositionForRemove = {
+ updateDragPositionForRemove = { offset ->
isDraggingToRemove =
- checkForDraggingToRemove(it, removeButtonCoordinates, gridCoordinates)
+ isPointerWithinCoordinates(
+ offset = gridCoordinates?.let { it.positionInWindow() + offset },
+ containerToCheck = removeButtonCoordinates
+ )
isDraggingToRemove
},
onOpenWidgetPicker = onOpenWidgetPicker,
+ gridState = gridState,
+ contentListState = contentListState,
+ selectedIndex = selectedIndex
)
if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) {
@@ -129,6 +176,14 @@
setRemoveButtonCoordinates = { removeButtonCoordinates = it },
onEditDone = onEditDone,
onOpenWidgetPicker = onOpenWidgetPicker,
+ onRemoveClicked = {
+ selectedIndex.value?.let { index ->
+ contentListState.onRemove(index)
+ contentListState.onSaveList()
+ viewModel.setSelectedIndex(null)
+ }
+ },
+ removeEnabled = removeButtonEnabled
)
} else {
IconButton(onClick = viewModel::onOpenWidgetEditor) {
@@ -158,16 +213,18 @@
communalContent: List<CommunalContentModel>,
viewModel: BaseCommunalViewModel,
contentPadding: PaddingValues,
+ selectedIndex: State<Int?>,
+ contentOffset: Offset,
+ gridState: LazyGridState,
+ contentListState: ContentListState,
setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit,
updateDragPositionForRemove: (offset: Offset) -> Boolean,
onOpenWidgetPicker: (() -> Unit)? = null,
) {
var gridModifier = Modifier.align(Alignment.CenterStart)
- val gridState = rememberLazyGridState()
var list = communalContent
var dragDropState: GridDragDropState? = null
if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) {
- val contentListState = rememberContentListState(list, viewModel)
list = contentListState.list
// for drag & drop operations within the communal hub grid
dragDropState =
@@ -179,7 +236,7 @@
gridModifier =
gridModifier
.fillMaxSize()
- .dragContainer(dragDropState, beforeContentPadding(contentPadding), viewModel)
+ .dragContainer(dragDropState, contentOffset, viewModel)
.onGloballyPositioned { setGridCoordinates(it) }
// for widgets dropped from other activities
val dragAndDropTargetState =
@@ -218,8 +275,10 @@
list[index].size.dp().value,
)
if (viewModel.isEditMode && dragDropState != null) {
+ val selected by remember(index) { derivedStateOf { index == selectedIndex.value } }
DraggableItem(
dragDropState = dragDropState,
+ selected = selected,
enabled = list[index] is CommunalContentModel.Widget,
index = index,
size = size
@@ -253,11 +312,19 @@
@Composable
private fun Toolbar(
isDraggingToRemove: Boolean,
+ removeEnabled: Boolean,
+ onRemoveClicked: () -> Unit,
setToolbarSize: (toolbarSize: IntSize) -> Unit,
setRemoveButtonCoordinates: (coordinates: LayoutCoordinates) -> Unit,
onOpenWidgetPicker: () -> Unit,
- onEditDone: () -> Unit,
+ onEditDone: () -> Unit
) {
+ val removeButtonAlpha: Float by
+ animateFloatAsState(
+ targetValue = if (removeEnabled) 1f else 0.5f,
+ label = "RemoveButtonAlphaAnimation"
+ )
+
Row(
modifier =
Modifier.fillMaxWidth()
@@ -301,13 +368,18 @@
}
} else {
OutlinedButton(
- // Button is disabled to make it non-clickable
- enabled = false,
- onClick = {},
- colors = ButtonDefaults.outlinedButtonColors(disabledContentColor = colors.primary),
+ enabled = removeEnabled,
+ onClick = onRemoveClicked,
+ colors =
+ ButtonDefaults.outlinedButtonColors(
+ contentColor = colors.primary,
+ disabledContentColor = colors.primary
+ ),
border = BorderStroke(width = 1.0.dp, color = colors.primary),
contentPadding = Dimensions.ButtonPadding,
- modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) }
+ modifier =
+ Modifier.graphicsLayer { alpha = removeButtonAlpha }
+ .onGloballyPositioned { setRemoveButtonCoordinates(it) }
) {
RemoveButtonContent(spacerModifier)
}
@@ -385,7 +457,7 @@
) {
when (model) {
is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier)
- is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size)
+ is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(size)
is CommunalContentModel.CtaTileInViewMode ->
CtaTileInViewModeContent(viewModel, size, modifier)
is CommunalContentModel.CtaTileInEditMode ->
@@ -396,11 +468,11 @@
}
}
-/** Presents a placeholder card for the new widget being dragged and dropping into the grid. */
+/** Creates an empty card used to highlight a particular spot on the grid. */
@Composable
-fun WidgetPlaceholderContent(size: SizeF) {
+fun HighlightedItem(size: SizeF, modifier: Modifier = Modifier) {
Card(
- modifier = Modifier.size(Dp(size.width), Dp(size.height)),
+ modifier = modifier.size(Dp(size.width), Dp(size.height)),
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
border = BorderStroke(3.dp, LocalAndroidColorScheme.current.tertiaryFixed),
shape = RoundedCornerShape(16.dp)
@@ -528,7 +600,7 @@
contentAlignment = Alignment.Center,
) {
AndroidView(
- modifier = modifier,
+ modifier = modifier.allowGestures(allowed = !viewModel.isEditMode),
factory = { context ->
// The AppWidgetHostView will inherit the interaction handler from the
// AppWidgetHost. So set the interaction handler here before creating the view, and
@@ -616,8 +688,8 @@
private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingInPx {
return with(LocalDensity.current) {
ContentPaddingInPx(
- startPadding = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(),
- topPadding = paddingValues.calculateTopPadding().toPx()
+ start = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(),
+ top = paddingValues.calculateTopPadding().toPx()
)
}
}
@@ -626,18 +698,15 @@
* Check whether the pointer position that the item is being dragged at is within the coordinates of
* the remove button in the toolbar. Returns true if the item is removable.
*/
-private fun checkForDraggingToRemove(
- offset: Offset,
- removeButtonCoordinates: LayoutCoordinates?,
- gridCoordinates: LayoutCoordinates?,
+private fun isPointerWithinCoordinates(
+ offset: Offset?,
+ containerToCheck: LayoutCoordinates?
): Boolean {
- if (removeButtonCoordinates == null || gridCoordinates == null) {
+ if (offset == null || containerToCheck == null) {
return false
}
- val pointer = gridCoordinates.positionInWindow() + offset
- val removeButton = removeButtonCoordinates.positionInWindow()
- return pointer.x in removeButton.x..removeButton.x + removeButtonCoordinates.size.width &&
- pointer.y in removeButton.y..removeButton.y + removeButtonCoordinates.size.height
+ val container = containerToCheck.boundsInWindow()
+ return container.contains(offset)
}
private fun CommunalContentSize.dp(): Dp {
@@ -648,7 +717,9 @@
}
}
-data class ContentPaddingInPx(val startPadding: Float, val topPadding: Float)
+data class ContentPaddingInPx(val start: Float, val top: Float) {
+ fun toOffset(): Offset = Offset(start, top)
+}
object Dimensions {
val CardWidth = 464.dp
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
index 979991d..45f98b8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
@@ -21,12 +21,12 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import com.android.systemui.communal.domain.model.CommunalContentModel
-import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
+import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
@Composable
fun rememberContentListState(
communalContent: List<CommunalContentModel>,
- viewModel: CommunalEditModeViewModel,
+ viewModel: BaseCommunalViewModel,
): ContentListState {
return remember(communalContent) {
ContentListState(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
index 1138221..a195953 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
@@ -17,6 +17,10 @@
package com.android.systemui.communal.ui.compose
import android.util.SizeF
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
@@ -32,6 +36,7 @@
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
@@ -39,6 +44,7 @@
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.zIndex
+import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
import com.android.systemui.communal.ui.compose.extensions.plus
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import kotlinx.coroutines.CoroutineScope
@@ -109,13 +115,10 @@
internal fun onDragStart(offset: Offset, contentOffset: Offset) {
state.layoutInfo.visibleItemsInfo
- .firstOrNull { item ->
- // grid item offset is based off grid content container so we need to deduct
- // before content padding from the initial pointer position
- contentListState.isItemEditable(item.index) &&
- (offset.x - contentOffset.x).toInt() in item.offset.x..item.offsetEnd.x &&
- (offset.y - contentOffset.y).toInt() in item.offset.y..item.offsetEnd.y
- }
+ .filter { item -> contentListState.isItemEditable(item.index) }
+ // grid item offset is based off grid content container so we need to deduct
+ // before content padding from the initial pointer position
+ .firstItemAtOffset(offset - contentOffset)
?.apply {
dragStartPointerOffset = offset - this.offset.toOffset()
draggingItemIndex = index
@@ -148,12 +151,11 @@
val middleOffset = startOffset + (endOffset - startOffset) / 2f
val targetItem =
- state.layoutInfo.visibleItemsInfo.find { item ->
- contentListState.isItemEditable(item.index) &&
- middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x &&
- middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y &&
- draggingItem.index != item.index
- }
+ state.layoutInfo.visibleItemsInfo
+ .asSequence()
+ .filter { item -> contentListState.isItemEditable(item.index) }
+ .filter { item -> draggingItem.index != item.index }
+ .firstItemAtOffset(middleOffset)
if (targetItem != null) {
val scrollToIndex =
@@ -208,32 +210,31 @@
fun Modifier.dragContainer(
dragDropState: GridDragDropState,
- beforeContentPadding: ContentPaddingInPx,
+ contentOffset: Offset,
viewModel: BaseCommunalViewModel,
): Modifier {
- return pointerInput(dragDropState, beforeContentPadding) {
- detectDragGesturesAfterLongPress(
- onDrag = { change, offset ->
- change.consume()
- dragDropState.onDrag(offset = offset)
- },
- onDragStart = { offset ->
- dragDropState.onDragStart(
- offset,
- Offset(beforeContentPadding.startPadding, beforeContentPadding.topPadding)
- )
- viewModel.onReorderWidgetStart()
- },
- onDragEnd = {
- dragDropState.onDragInterrupted()
- viewModel.onReorderWidgetEnd()
- },
- onDragCancel = {
- dragDropState.onDragInterrupted()
- viewModel.onReorderWidgetCancel()
- }
- )
- }
+ return this.then(
+ pointerInput(dragDropState, contentOffset) {
+ detectDragGesturesAfterLongPress(
+ onDrag = { change, offset ->
+ change.consume()
+ dragDropState.onDrag(offset = offset)
+ },
+ onDragStart = { offset ->
+ dragDropState.onDragStart(offset, contentOffset)
+ viewModel.onReorderWidgetStart()
+ },
+ onDragEnd = {
+ dragDropState.onDragInterrupted()
+ viewModel.onReorderWidgetEnd()
+ },
+ onDragCancel = {
+ dragDropState.onDragInterrupted()
+ viewModel.onReorderWidgetCancel()
+ }
+ )
+ }
+ )
}
/** Wrap LazyGrid item with additional modifier needed for drag and drop. */
@@ -243,6 +244,7 @@
dragDropState: GridDragDropState,
index: Int,
enabled: Boolean,
+ selected: Boolean,
size: SizeF,
modifier: Modifier = Modifier,
content: @Composable (isDragging: Boolean) -> Unit
@@ -250,21 +252,31 @@
if (!enabled) {
return Box(modifier = modifier) { content(false) }
}
+
val dragging = index == dragDropState.draggingItemIndex
+ val itemAlpha: Float by
+ animateFloatAsState(
+ targetValue = if (dragDropState.isDraggingToRemove) 0.5f else 1f,
+ label = "DraggableItemAlpha"
+ )
val draggingModifier =
if (dragging) {
Modifier.zIndex(1f).graphicsLayer {
translationX = dragDropState.draggingItemOffset.x
translationY = dragDropState.draggingItemOffset.y
- alpha = if (dragDropState.isDraggingToRemove) 0.5f else 1f
+ alpha = itemAlpha
}
} else {
Modifier.animateItemPlacement()
}
Box(modifier) {
- if (dragging) {
- WidgetPlaceholderContent(size)
+ AnimatedVisibility(
+ visible = (dragging || selected) && !dragDropState.isDraggingToRemove,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ HighlightedItem(size)
}
Box(modifier = draggingModifier, propagateMinConstraints = true) { content(dragging) }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt
new file mode 100644
index 0000000..132093f
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.ui.compose.extensions
+
+import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.toRect
+
+/**
+ * Determine the item at the specified offset, or null if none exist.
+ *
+ * @param offset The offset in pixels, relative to the top start of the grid.
+ */
+fun Iterable<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? =
+ firstOrNull { item ->
+ isItemAtOffset(item, offset)
+ }
+
+/**
+ * Determine the item at the specified offset, or null if none exist.
+ *
+ * @param offset The offset in pixels, relative to the top start of the grid.
+ */
+fun Sequence<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? =
+ firstOrNull { item ->
+ isItemAtOffset(item, offset)
+ }
+
+private fun isItemAtOffset(item: LazyGridItemInfo, offset: Offset): Boolean {
+ val boundingBox = IntRect(item.offset, item.size)
+ return boundingBox.toRect().contains(offset)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt
new file mode 100644
index 0000000..b31008e
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.ui.compose.extensions
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+
+/** Sets whether gestures are allowed on children of this element. */
+fun Modifier.allowGestures(allowed: Boolean): Modifier =
+ if (allowed) {
+ this
+ } else {
+ this.then(pointerInput(Unit) { consumeAllGestures() })
+ }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt
new file mode 100644
index 0000000..1407494
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.ui.compose.extensions
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import kotlinx.coroutines.coroutineScope
+
+/**
+ * Observe taps without actually consuming them, so child elements can still respond to them. Long
+ * presses are excluded.
+ */
+suspend fun PointerInputScope.observeTapsWithoutConsuming(
+ pass: PointerEventPass = PointerEventPass.Initial,
+ onTap: ((Offset) -> Unit)? = null,
+) = coroutineScope {
+ if (onTap == null) return@coroutineScope
+ awaitEachGesture {
+ awaitFirstDown(pass = pass)
+ val tapTimeout = viewConfiguration.longPressTimeoutMillis
+ val up = withTimeoutOrNull(tapTimeout) { waitForUpOrCancellation(pass = pass) }
+ if (up != null) {
+ onTap(up.position)
+ }
+ }
+}
+
+/** Consume all gestures on the initial pass so that child elements do not receive them. */
+suspend fun PointerInputScope.consumeAllGestures() = coroutineScope {
+ awaitEachGesture {
+ awaitPointerEvent(pass = PointerEventPass.Initial)
+ .changes
+ .forEach(PointerInputChange::consume)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index 84d4246..56d6879 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -17,13 +17,16 @@
package com.android.systemui.keyguard.ui.composable.blueprint
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntRect
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
@@ -66,6 +69,7 @@
override fun SceneScope.Content(modifier: Modifier) {
val isUdfpsVisible = viewModel.isUdfpsVisible
val burnIn = rememberBurnIn(clockInteractor)
+ val resources = LocalContext.current.resources
LockscreenLongPress(
viewModel = viewModel.longPress,
@@ -88,13 +92,27 @@
SmartSpace(
burnInParams = burnIn.parameters,
onTopChanged = burnIn.onSmartspaceTopChanged,
- modifier = Modifier.fillMaxWidth(),
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(
+ top = { viewModel.getSmartSpacePaddingTop(resources) }
+ ),
)
}
- with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
- with(notificationSection) {
- Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
+
+ if (viewModel.isLargeClockVisible) {
+ Spacer(modifier = Modifier.weight(weight = 1f))
+ with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
}
+
+ if (viewModel.areNotificationsVisible) {
+ with(notificationSection) {
+ Notifications(
+ modifier = Modifier.fillMaxWidth().weight(weight = 1f)
+ )
+ }
+ }
+
if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
with(ambientIndicationSectionOptional.get()) {
AmbientIndication(modifier = Modifier.fillMaxWidth())
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index 4148462..d0aa444 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -17,13 +17,16 @@
package com.android.systemui.keyguard.ui.composable.blueprint
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntRect
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
@@ -66,6 +69,7 @@
override fun SceneScope.Content(modifier: Modifier) {
val isUdfpsVisible = viewModel.isUdfpsVisible
val burnIn = rememberBurnIn(clockInteractor)
+ val resources = LocalContext.current.resources
LockscreenLongPress(
viewModel = viewModel.longPress,
@@ -88,13 +92,27 @@
SmartSpace(
burnInParams = burnIn.parameters,
onTopChanged = burnIn.onSmartspaceTopChanged,
- modifier = Modifier.fillMaxWidth(),
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(
+ top = { viewModel.getSmartSpacePaddingTop(resources) }
+ ),
)
}
- with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
- with(notificationSection) {
- Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
+
+ if (viewModel.isLargeClockVisible) {
+ Spacer(modifier = Modifier.weight(weight = 1f))
+ with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
}
+
+ if (viewModel.areNotificationsVisible) {
+ with(notificationSection) {
+ Notifications(
+ modifier = Modifier.fillMaxWidth().weight(weight = 1f)
+ )
+ }
+ }
+
if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
with(ambientIndicationSectionOptional.get()) {
AmbientIndication(modifier = Modifier.fillMaxWidth())
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
index f021bb6..f40b871 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
@@ -30,10 +30,10 @@
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.padding
import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.customization.R as customizationR
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.res.R
import javax.inject.Inject
class ClockSection
@@ -79,7 +79,7 @@
modifier =
Modifier.padding(
horizontal =
- dimensionResource(R.dimen.keyguard_affordance_horizontal_offset)
+ dimensionResource(customizationR.dimen.clock_padding_start)
)
.padding(top = { viewModel.getSmallClockTopMargin(view.context) })
.onTopPlacementChanged(onTopChanged),
@@ -117,9 +117,7 @@
content {
AndroidView(
factory = { checkNotNull(currentClock).largeClock.view },
- modifier =
- Modifier.fillMaxWidth()
- .padding(top = { viewModel.getLargeClockTopMargin(view.context) })
+ modifier = Modifier.fillMaxWidth()
)
}
}
diff --git a/packages/SystemUI/compose/features/tests/AndroidManifest.xml b/packages/SystemUI/compose/features/tests/AndroidManifest.xml
index 8fe9656..fc337fb 100644
--- a/packages/SystemUI/compose/features/tests/AndroidManifest.xml
+++ b/packages/SystemUI/compose/features/tests/AndroidManifest.xml
@@ -30,11 +30,6 @@
android:enabled="false"
tools:replace="android:authorities"
tools:node="remove" />
- <provider android:name="com.google.android.systemui.keyguard.KeyguardSliceProviderGoogle"
- android:authorities="com.android.systemui.test.keyguard.disabled"
- android:enabled="false"
- tools:replace="android:authorities"
- tools:node="remove" />
<provider android:name="com.android.systemui.keyguard.CustomizationProvider"
android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled"
android:enabled="false"
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 64388b7..ff05478 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -73,7 +73,7 @@
private val positionalThreshold
get() = with(layoutImpl.density) { 56.dp.toPx() }
- internal var gestureWithPriority: Any? = null
+ internal var currentSource: Any? = null
/** The [UserAction]s associated to the current swipe. */
private var actionUpOrLeft: UserAction? = null
@@ -520,20 +520,22 @@
private class SceneDraggableHandler(
private val gestureHandler: SceneGestureHandler,
) : DraggableHandler {
+ private val source = this
+
override fun onDragStarted(startedPosition: Offset, overSlop: Float, pointersDown: Int) {
- gestureHandler.gestureWithPriority = this
+ gestureHandler.currentSource = source
gestureHandler.onDragStarted(pointersDown, startedPosition, overSlop)
}
override fun onDelta(pixels: Float) {
- if (gestureHandler.gestureWithPriority == this) {
+ if (gestureHandler.currentSource == source) {
gestureHandler.onDrag(delta = pixels)
}
}
override fun onDragStopped(velocity: Float) {
- if (gestureHandler.gestureWithPriority == this) {
- gestureHandler.gestureWithPriority = null
+ if (gestureHandler.currentSource == source) {
+ gestureHandler.currentSource = null
gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
}
}
@@ -586,6 +588,8 @@
return nextScene != null
}
+ val source = this
+
return PriorityNestedScrollConnection(
orientation = orientation,
canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
@@ -656,7 +660,7 @@
canContinueScroll = { true },
canScrollOnFling = false,
onStart = { offsetAvailable ->
- gestureHandler.gestureWithPriority = this
+ gestureHandler.currentSource = source
gestureHandler.onDragStarted(
pointersDown = 1,
startedPosition = null,
@@ -664,7 +668,7 @@
)
},
onScroll = { offsetAvailable ->
- if (gestureHandler.gestureWithPriority != this) {
+ if (gestureHandler.currentSource != source) {
return@PriorityNestedScrollConnection 0f
}
@@ -675,7 +679,7 @@
offsetAvailable
},
onStop = { velocityAvailable ->
- if (gestureHandler.gestureWithPriority != this) {
+ if (gestureHandler.currentSource != source) {
return@PriorityNestedScrollConnection 0f
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index e893169..c4bcb53 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -35,6 +35,7 @@
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository;
import com.android.systemui.res.R;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -107,7 +108,7 @@
mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener,
mEmergencyButtonController, mFalsingCollector, featureFlags,
- mSelectedUserInteractor) {
+ mSelectedUserInteractor, new FakeKeyboardRepository()) {
@Override
public void onResume(int reason) {
super.onResume(reason);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 78b854e..c2efc05 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -33,6 +33,7 @@
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.DevicePostureController
import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED
@@ -141,7 +142,8 @@
postureController,
featureFlags,
mSelectedUserInteractor,
- uiEventLogger
+ uiEventLogger,
+ FakeKeyboardRepository()
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
index f775175..0959f1b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
@@ -16,7 +16,6 @@
package com.android.keyguard
-import android.telephony.PinResult
import android.telephony.TelephonyManager
import android.testing.TestableLooper
import android.view.LayoutInflater
@@ -28,9 +27,11 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
import com.android.systemui.res.R
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -39,7 +40,6 @@
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
@@ -75,8 +75,7 @@
`when`(messageAreaControllerFactory.create(Mockito.any(KeyguardMessageArea::class.java)))
.thenReturn(keyguardMessageAreaController)
`when`(telephonyManager.createForSubscriptionId(anyInt())).thenReturn(telephonyManager)
- `when`(telephonyManager.supplyIccLockPin(anyString()))
- .thenReturn(mock(PinResult::class.java))
+ `when`(telephonyManager.supplyIccLockPin(anyString())).thenReturn(mock())
simPinView =
LayoutInflater.from(context).inflate(R.layout.keyguard_sim_pin_view, null)
as KeyguardSimPinView
@@ -97,7 +96,8 @@
falsingCollector,
emergencyButtonController,
fakeFeatureFlags,
- mSelectedUserInteractor
+ mSelectedUserInteractor,
+ FakeKeyboardRepository()
)
underTest.init()
underTest.onViewAttached()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
index 45a60199..1281e44 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
import com.android.systemui.res.R
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.mockito.any
@@ -91,6 +92,7 @@
emergencyButtonController,
fakeFeatureFlags,
mSelectedUserInteractor,
+ FakeKeyboardRepository()
)
underTest.init()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 4f7d944..6828041 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -21,23 +21,24 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertEquals
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -46,18 +47,11 @@
@kotlinx.coroutines.ExperimentalCoroutinesApi
class KeyguardTransitionInteractorTest : SysuiTestCase() {
- private lateinit var underTest: KeyguardTransitionInteractor
- private lateinit var repository: FakeKeyguardTransitionRepository
- private val testScope = TestScope()
+ val kosmos = testKosmos()
- @Before
- fun setUp() {
- repository = FakeKeyguardTransitionRepository()
- underTest = KeyguardTransitionInteractorFactory.create(
- scope = testScope.backgroundScope,
- repository = repository,
- ).keyguardTransitionInteractor
- }
+ val underTest = kosmos.keyguardTransitionInteractor
+ val repository = kosmos.fakeKeyguardTransitionRepository
+ val testScope = kosmos.testScope
@Test
fun transitionCollectorsReceivesOnlyAppropriateEvents() = runTest {
@@ -114,48 +108,50 @@
}
@Test
- fun finishedKeyguardStateTests() = testScope.runTest {
- val finishedSteps by collectValues(underTest.finishedKeyguardState)
- runCurrent()
- val steps = mutableListOf<TransitionStep>()
-
- steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED))
- steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING))
- steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED))
- steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED))
- steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING))
- steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED))
- steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
-
- steps.forEach {
- repository.sendTransitionStep(it)
+ fun finishedKeyguardStateTests() =
+ testScope.runTest {
+ val finishedSteps by collectValues(underTest.finishedKeyguardState)
runCurrent()
- }
+ val steps = mutableListOf<TransitionStep>()
- assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD))
- }
+ steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED))
+ steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING))
+ steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED))
+ steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED))
+ steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING))
+ steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED))
+ steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
+
+ steps.forEach {
+ repository.sendTransitionStep(it)
+ runCurrent()
+ }
+
+ assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD))
+ }
@Test
- fun startedKeyguardStateTests() = testScope.runTest {
- val startedStates by collectValues(underTest.startedKeyguardState)
- runCurrent()
- val steps = mutableListOf<TransitionStep>()
-
- steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED))
- steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING))
- steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED))
- steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED))
- steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING))
- steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED))
- steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
-
- steps.forEach {
- repository.sendTransitionStep(it)
+ fun startedKeyguardStateTests() =
+ testScope.runTest {
+ val startedStates by collectValues(underTest.startedKeyguardState)
runCurrent()
- }
+ val steps = mutableListOf<TransitionStep>()
- assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE))
- }
+ steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED))
+ steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING))
+ steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED))
+ steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED))
+ steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING))
+ steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED))
+ steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
+
+ steps.forEach {
+ repository.sendTransitionStep(it)
+ runCurrent()
+ }
+
+ assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE))
+ }
@Test
fun finishedKeyguardTransitionStepTests() = runTest {
@@ -178,7 +174,7 @@
// Ignore the default state.
assertThat(finishedSteps.subList(1, finishedSteps.size))
- .isEqualTo(listOf(steps[2], steps[5]))
+ .isEqualTo(listOf(steps[2], steps[5]))
}
@Test
@@ -233,500 +229,1067 @@
}
@Test
- fun isInTransitionToState() = testScope.runTest {
- val results by collectValues(underTest.isInTransitionToState(GONE))
+ fun isInTransitionToAnyState() =
+ testScope.runTest {
+ val inTransition by collectValues(underTest.isInTransitionToAnyState)
- sendSteps(
+ assertEquals(
+ listOf(
+ true, // The repo is seeded with a transition from OFF to LOCKSCREEN.
+ false,
+ ),
+ inTransition
+ )
+
+ sendSteps(
+ TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+ )
+
+ assertEquals(
+ listOf(
+ true,
+ false,
+ true,
+ ),
+ inTransition
+ )
+
+ sendSteps(
+ TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+ )
+
+ assertEquals(
+ listOf(
+ true,
+ false,
+ true,
+ ),
+ inTransition
+ )
+
+ sendSteps(
+ TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
+ )
+
+ assertEquals(
+ listOf(
+ true,
+ false,
+ true,
+ false,
+ ),
+ inTransition
+ )
+ }
+
+ @Test
+ fun isInTransitionToAnyState_finishedStateIsStartedStateAfterCancels() =
+ testScope.runTest {
+ val inTransition by collectValues(underTest.isInTransitionToAnyState)
+
+ assertEquals(
+ listOf(
+ true,
+ false,
+ ),
+ inTransition
+ )
+
+ // Start FINISHED in GONE.
+ sendSteps(
+ TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+ TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+ TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
+ )
+
+ assertEquals(
+ listOf(
+ true,
+ false,
+ true,
+ false,
+ ),
+ inTransition
+ )
+
+ sendSteps(
+ TransitionStep(GONE, DOZING, 0f, STARTED),
+ )
+
+ assertEquals(
+ listOf(
+ true,
+ false,
+ true,
+ false,
+ true,
+ ),
+ inTransition
+ )
+
+ sendSteps(
+ TransitionStep(GONE, DOZING, 0.5f, RUNNING),
+ TransitionStep(GONE, DOZING, 0.6f, CANCELED),
+ TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED),
+ TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING),
+ TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED),
+ TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+ )
+
+ assertEquals(
+ listOf(
+ true,
+ false,
+ true,
+ false,
+ // We should have been in transition throughout the entire transition, including
+ // both cancellations, and we should still be in transition despite now
+ // transitioning to GONE, the state we're also FINISHED in.
+ true,
+ ),
+ inTransition
+ )
+
+ sendSteps(
+ TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+ TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
+ )
+
+ assertEquals(
+ listOf(
+ true,
+ false,
+ true,
+ false,
+ true,
+ false,
+ ),
+ inTransition
+ )
+ }
+
+ @Test
+ fun isInTransitionToState() =
+ testScope.runTest {
+ val results by collectValues(underTest.isInTransitionToState(GONE))
+
+ sendSteps(
TransitionStep(AOD, DOZING, 0f, STARTED),
TransitionStep(AOD, DOZING, 0.5f, RUNNING),
TransitionStep(AOD, DOZING, 1f, FINISHED),
- )
+ )
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- ))
-
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, FINISHED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(GONE, DOZING, 0f, STARTED),
TransitionStep(GONE, DOZING, 0f, RUNNING),
TransitionStep(GONE, DOZING, 1f, FINISHED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- true,
- ))
- }
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ true,
+ )
+ )
+ }
@Test
- fun isInTransitionFromState() = testScope.runTest {
- val results by collectValues(underTest.isInTransitionFromState(DOZING))
+ fun isInTransitionFromState() =
+ testScope.runTest {
+ val results by collectValues(underTest.isInTransitionFromState(DOZING))
- sendSteps(
+ sendSteps(
TransitionStep(AOD, DOZING, 0f, STARTED),
TransitionStep(AOD, DOZING, 0.5f, RUNNING),
TransitionStep(AOD, DOZING, 1f, FINISHED),
- )
+ )
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- ))
-
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, FINISHED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(GONE, DOZING, 0f, STARTED),
TransitionStep(GONE, DOZING, 0f, RUNNING),
TransitionStep(GONE, DOZING, 1f, FINISHED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- true,
- ))
- }
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ true,
+ )
+ )
+ }
@Test
- fun isInTransitionFromStateWhere() = testScope.runTest {
- val results by collectValues(underTest.isInTransitionFromStateWhere {
- it == DOZING
- })
+ fun isInTransitionFromStateWhere() =
+ testScope.runTest {
+ val results by collectValues(underTest.isInTransitionFromStateWhere { it == DOZING })
- sendSteps(
+ sendSteps(
TransitionStep(AOD, DOZING, 0f, STARTED),
TransitionStep(AOD, DOZING, 0.5f, RUNNING),
TransitionStep(AOD, DOZING, 1f, FINISHED),
- )
+ )
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- ))
-
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, FINISHED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(GONE, DOZING, 0f, STARTED),
TransitionStep(GONE, DOZING, 0f, RUNNING),
TransitionStep(GONE, DOZING, 1f, FINISHED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- true,
- ))
- }
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ true,
+ )
+ )
+ }
@Test
- fun isInTransitionWhere() = testScope.runTest {
- val results by collectValues(underTest.isInTransitionWhere(
- fromStatePredicate = { it == DOZING },
- toStatePredicate = { it == GONE },
- ))
+ fun isInTransitionWhere() =
+ testScope.runTest {
+ val results by
+ collectValues(
+ underTest.isInTransitionWhere(
+ fromStatePredicate = { it == DOZING },
+ toStatePredicate = { it == GONE },
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(AOD, DOZING, 0f, STARTED),
TransitionStep(AOD, DOZING, 0.5f, RUNNING),
TransitionStep(AOD, DOZING, 1f, FINISHED),
- )
+ )
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- ))
-
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, FINISHED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(GONE, DOZING, 0f, STARTED),
TransitionStep(GONE, DOZING, 0f, RUNNING),
TransitionStep(GONE, DOZING, 1f, FINISHED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- true,
- ))
- }
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ true,
+ )
+ )
+ }
@Test
- fun isFinishedInStateWhere() = testScope.runTest {
- val results by collectValues(underTest.isFinishedInStateWhere { it == GONE } )
+ fun isInTransitionWhere_withCanceledStep() =
+ testScope.runTest {
+ val results by
+ collectValues(
+ underTest.isInTransitionWhere(
+ fromStatePredicate = { it == DOZING },
+ toStatePredicate = { it == GONE },
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(AOD, DOZING, 0f, STARTED),
TransitionStep(AOD, DOZING, 0.5f, RUNNING),
TransitionStep(AOD, DOZING, 1f, FINISHED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false, // Finished in DOZING, not GONE.
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ )
+ )
- sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
+ sendSteps(
+ TransitionStep(DOZING, GONE, 0f, STARTED),
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
+ sendSteps(
+ TransitionStep(DOZING, GONE, 0f, RUNNING),
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
+ sendSteps(
+ TransitionStep(DOZING, GONE, 0f, CANCELED),
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(GONE, DOZING, 0f, STARTED),
TransitionStep(GONE, DOZING, 0f, RUNNING),
- )
+ TransitionStep(GONE, DOZING, 1f, FINISHED),
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED))
-
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
-
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
-
- sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
-
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- true,
- ))
- }
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ true,
+ )
+ )
+ }
@Test
- fun isFinishedInState() = testScope.runTest {
- val results by collectValues(underTest.isFinishedInState(GONE))
+ fun isFinishedInStateWhere() =
+ testScope.runTest {
+ val results by collectValues(underTest.isFinishedInStateWhere { it == GONE })
- sendSteps(
+ sendSteps(
TransitionStep(AOD, DOZING, 0f, STARTED),
TransitionStep(AOD, DOZING, 0.5f, RUNNING),
TransitionStep(AOD, DOZING, 1f, FINISHED),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false, // Finished in DOZING, not GONE.
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false, // Finished in DOZING, not GONE.
+ )
+ )
- sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
+ sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
- assertThat(results).isEqualTo(listOf(
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ )
+ )
- sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
+ sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
- assertThat(results).isEqualTo(listOf(
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ )
+ )
- sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
+ sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(GONE, DOZING, 0f, STARTED),
TransitionStep(GONE, DOZING, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
- sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED))
+ sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED))
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(
+ sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ )
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- ))
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
- sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
+ sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
- assertThat(results).isEqualTo(listOf(
- false,
- true,
- false,
- true,
- ))
- }
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ true,
+ )
+ )
+ }
@Test
- fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() = testScope.runTest {
- val finishedStates by collectValues(underTest.finishedKeyguardState)
+ fun isFinishedInState() =
+ testScope.runTest {
+ val results by collectValues(underTest.isFinishedInState(GONE))
- // We default FINISHED in LOCKSCREEN.
- assertEquals(listOf(
- LOCKSCREEN
- ), finishedStates)
+ sendSteps(
+ TransitionStep(AOD, DOZING, 0f, STARTED),
+ TransitionStep(AOD, DOZING, 0.5f, RUNNING),
+ TransitionStep(AOD, DOZING, 1f, FINISHED),
+ )
- sendSteps(
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false, // Finished in DOZING, not GONE.
+ )
+ )
+
+ sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
+
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ )
+ )
+
+ sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
+
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ )
+ )
+
+ sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
+
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
+
+ sendSteps(
+ TransitionStep(GONE, DOZING, 0f, STARTED),
+ TransitionStep(GONE, DOZING, 0f, RUNNING),
+ )
+
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ )
+ )
+
+ sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED))
+
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
+
+ sendSteps(
+ TransitionStep(DOZING, GONE, 0f, STARTED),
+ TransitionStep(DOZING, GONE, 0f, RUNNING),
+ )
+
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ )
+ )
+
+ sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
+
+ assertThat(results)
+ .isEqualTo(
+ listOf(
+ false,
+ true,
+ false,
+ true,
+ )
+ )
+ }
+
+ @Test
+ fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() =
+ testScope.runTest {
+ val finishedStates by collectValues(underTest.finishedKeyguardState)
+
+ // We default FINISHED in LOCKSCREEN.
+ assertEquals(listOf(LOCKSCREEN), finishedStates)
+
+ sendSteps(
TransitionStep(LOCKSCREEN, AOD, 0f, STARTED),
TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING),
TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED),
- )
+ )
- // We're FINISHED in AOD.
- assertEquals(listOf(
- LOCKSCREEN,
- AOD,
- ), finishedStates)
+ // We're FINISHED in AOD.
+ assertEquals(
+ listOf(
+ LOCKSCREEN,
+ AOD,
+ ),
+ finishedStates
+ )
- // Transition back to LOCKSCREEN.
- sendSteps(
+ // Transition back to LOCKSCREEN.
+ sendSteps(
TransitionStep(AOD, LOCKSCREEN, 0f, STARTED),
TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING),
TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED),
- )
+ )
- // We're FINISHED in LOCKSCREEN.
- assertEquals(listOf(
- LOCKSCREEN,
- AOD,
- LOCKSCREEN,
- ), finishedStates)
+ // We're FINISHED in LOCKSCREEN.
+ assertEquals(
+ listOf(
+ LOCKSCREEN,
+ AOD,
+ LOCKSCREEN,
+ ),
+ finishedStates
+ )
- sendSteps(
+ sendSteps(
TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
- )
+ )
- // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in
- // LOCKSCREEN.
- assertEquals(listOf(
- LOCKSCREEN,
- AOD,
- LOCKSCREEN,
- ), finishedStates)
+ // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in
+ // LOCKSCREEN.
+ assertEquals(
+ listOf(
+ LOCKSCREEN,
+ AOD,
+ LOCKSCREEN,
+ ),
+ finishedStates
+ )
- sendSteps(
+ sendSteps(
TransitionStep(LOCKSCREEN, GONE, 0.6f, CANCELED),
- )
+ )
- // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN.
- assertEquals(listOf(
- LOCKSCREEN,
- AOD,
- LOCKSCREEN,
- ), finishedStates)
+ // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN.
+ assertEquals(
+ listOf(
+ LOCKSCREEN,
+ AOD,
+ LOCKSCREEN,
+ ),
+ finishedStates
+ )
- sendSteps(
+ sendSteps(
TransitionStep(GONE, LOCKSCREEN, 0.6f, STARTED),
TransitionStep(GONE, LOCKSCREEN, 0.9f, RUNNING),
TransitionStep(GONE, LOCKSCREEN, 1f, FINISHED),
- )
+ )
- // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to
- // LOCKSCREEN after the cancellation.
- assertEquals(listOf(
- LOCKSCREEN,
- AOD,
- LOCKSCREEN,
- LOCKSCREEN,
- ), finishedStates)
- }
+ // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to
+ // LOCKSCREEN after the cancellation.
+ assertEquals(
+ listOf(
+ LOCKSCREEN,
+ AOD,
+ LOCKSCREEN,
+ LOCKSCREEN,
+ ),
+ finishedStates
+ )
+ }
+
+ @Test
+ fun testCurrentState() =
+ testScope.runTest {
+ val currentStates by collectValues(underTest.currentKeyguardState)
+
+ // We init the repo with a transition from OFF -> LOCKSCREEN.
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(LOCKSCREEN, AOD, 0f, STARTED),
+ )
+
+ // The current state should continue to be LOCKSCREEN as we transition to AOD.
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING),
+ )
+
+ // The current state should continue to be LOCKSCREEN as we transition to AOD.
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(LOCKSCREEN, AOD, 0.6f, CANCELED),
+ )
+
+ // Once CANCELED, we're still currently in LOCKSCREEN...
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(AOD, LOCKSCREEN, 0.6f, STARTED),
+ )
+
+ // ...until STARTING back to LOCKSCREEN, at which point the "current" state should be
+ // the
+ // one we're transitioning from, despite never FINISHING in that state.
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ AOD,
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(AOD, LOCKSCREEN, 0.8f, RUNNING),
+ TransitionStep(AOD, LOCKSCREEN, 0.8f, FINISHED),
+ )
+
+ // FINSHING in LOCKSCREEN should update the current state to LOCKSCREEN.
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ AOD,
+ LOCKSCREEN,
+ ),
+ currentStates
+ )
+ }
+
+ @Test
+ fun testCurrentState_multipleCancellations_backToLastFinishedState() =
+ testScope.runTest {
+ val currentStates by collectValues(underTest.currentKeyguardState)
+
+ // We init the repo with a transition from OFF -> LOCKSCREEN.
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+ TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+ TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
+ )
+
+ assertEquals(
+ listOf(
+ // Default transition from OFF -> LOCKSCREEN
+ OFF,
+ LOCKSCREEN,
+ // Transitioned to GONE
+ GONE,
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(GONE, DOZING, 0f, STARTED),
+ TransitionStep(GONE, DOZING, 0.5f, RUNNING),
+ TransitionStep(GONE, DOZING, 0.6f, CANCELED),
+ )
+
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ GONE,
+ // Current state should not be DOZING until the post-cancelation transition is
+ // STARTED
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED),
+ )
+
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ GONE,
+ // DOZING -> LS STARTED, DOZING is now the current state.
+ DOZING,
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING),
+ TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED),
+ )
+
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ GONE,
+ DOZING,
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+ )
+
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ GONE,
+ DOZING,
+ // LS -> GONE STARTED, LS is now the current state.
+ LOCKSCREEN,
+ ),
+ currentStates
+ )
+
+ sendSteps(
+ TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+ TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
+ )
+
+ assertEquals(
+ listOf(
+ OFF,
+ LOCKSCREEN,
+ GONE,
+ DOZING,
+ LOCKSCREEN,
+ // FINISHED in GONE, GONE is now the current state.
+ GONE,
+ ),
+ currentStates
+ )
+ }
private suspend fun sendSteps(vararg steps: TransitionStep) {
steps.forEach {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
new file mode 100644
index 0000000..d4dd2ac
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.authController
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LockscreenContentViewModelTest : SysuiTestCase() {
+
+ private val kosmos: Kosmos = testKosmos()
+
+ lateinit var underTest: LockscreenContentViewModel
+
+ @Before
+ fun setup() {
+ with(kosmos) {
+ fakeFeatureFlagsClassic.set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, true)
+ underTest = lockscreenContentViewModel
+ }
+ }
+
+ @Test
+ fun isUdfpsVisible_withUdfps_true() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(kosmos.authController.isUdfpsSupported).thenReturn(true)
+ assertThat(underTest.isUdfpsVisible).isTrue()
+ }
+ }
+
+ @Test
+ fun isUdfpsVisible_withoutUdfps_false() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(kosmos.authController.isUdfpsSupported).thenReturn(false)
+ assertThat(underTest.isUdfpsVisible).isFalse()
+ }
+ }
+
+ @Test
+ fun isLargeClockVisible_withLargeClock_true() =
+ with(kosmos) {
+ testScope.runTest {
+ kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE)
+ assertThat(underTest.isLargeClockVisible).isTrue()
+ }
+ }
+
+ @Test
+ fun isLargeClockVisible_withSmallClock_false() =
+ with(kosmos) {
+ testScope.runTest {
+ kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.SMALL)
+ assertThat(underTest.isLargeClockVisible).isFalse()
+ }
+ }
+
+ @Test
+ fun areNotificationsVisible_withSmallClock_true() =
+ with(kosmos) {
+ testScope.runTest {
+ kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.SMALL)
+ assertThat(underTest.areNotificationsVisible).isTrue()
+ }
+ }
+
+ @Test
+ fun areNotificationsVisible_withLargeClock_false() =
+ with(kosmos) {
+ testScope.runTest {
+ kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE)
+ assertThat(underTest.areNotificationsVisible).isFalse()
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
index 070e07a..eb845b2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
@@ -17,11 +17,9 @@
package com.android.systemui.qs.pipeline.data.repository
import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE
-import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.IntentFilter
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
@@ -33,44 +31,36 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.fakePackageChangeRepository
+import com.android.systemui.common.data.repository.packageChangeRepository
+import com.android.systemui.common.data.shared.model.PackageChangeModel
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argThat
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Captor
import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
-@OptIn(ExperimentalCoroutinesApi::class)
class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() {
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
@Mock private lateinit var context: Context
@Mock private lateinit var packageManager: PackageManager
- @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
private lateinit var underTest: InstalledTilesComponentRepositoryImpl
@@ -92,63 +82,12 @@
underTest =
InstalledTilesComponentRepositoryImpl(
context,
- testDispatcher,
+ kosmos.testDispatcher,
+ kosmos.packageChangeRepository
)
}
@Test
- fun registersAndUnregistersBroadcastReceiver() =
- testScope.runTest {
- val user = 10
- val job = launch { underTest.getInstalledTilesComponents(user).collect {} }
- runCurrent()
-
- verify(context)
- .registerReceiverAsUser(
- capture(receiverCaptor),
- eq(UserHandle.of(user)),
- any(),
- nullable(),
- nullable(),
- )
-
- verify(context, never()).unregisterReceiver(receiverCaptor.value)
-
- job.cancel()
- runCurrent()
- verify(context).unregisterReceiver(receiverCaptor.value)
- }
-
- @Test
- fun intentFilterForCorrectActionsAndScheme() =
- testScope.runTest {
- val filterCaptor = argumentCaptor<IntentFilter>()
-
- backgroundScope.launch { underTest.getInstalledTilesComponents(0).collect {} }
- runCurrent()
-
- verify(context)
- .registerReceiverAsUser(
- any(),
- any(),
- capture(filterCaptor),
- nullable(),
- nullable(),
- )
-
- with(filterCaptor.value) {
- assertThat(matchAction(Intent.ACTION_PACKAGE_CHANGED)).isTrue()
- assertThat(matchAction(Intent.ACTION_PACKAGE_ADDED)).isTrue()
- assertThat(matchAction(Intent.ACTION_PACKAGE_REMOVED)).isTrue()
- assertThat(matchAction(Intent.ACTION_PACKAGE_REPLACED)).isTrue()
- assertThat(countActions()).isEqualTo(4)
-
- assertThat(hasDataScheme("package")).isTrue()
- assertThat(countDataSchemes()).isEqualTo(1)
- }
- }
-
- @Test
fun componentsLoadedOnStart() =
testScope.runTest {
val userId = 0
@@ -169,7 +108,7 @@
}
@Test
- fun componentAdded_foundAfterBroadcast() =
+ fun componentAdded_foundAfterPackageChange() =
testScope.runTest {
val userId = 0
val resolveInfo =
@@ -186,7 +125,7 @@
)
)
.thenReturn(listOf(resolveInfo))
- getRegisteredReceiver().onReceive(context, Intent(Intent.ACTION_PACKAGE_ADDED))
+ kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty)
assertThat(componentNames).containsExactly(TEST_COMPONENT)
}
@@ -275,19 +214,6 @@
assertThat(componentNames).containsExactly(TEST_COMPONENT)
}
- private fun getRegisteredReceiver(): BroadcastReceiver {
- verify(context)
- .registerReceiverAsUser(
- capture(receiverCaptor),
- any(),
- any(),
- nullable(),
- nullable(),
- )
-
- return receiverCaptor.value
- }
-
companion object {
private val INTENT = Intent(TileService.ACTION_QS_TILE)
private val FLAGS =
diff --git a/packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.xml b/packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.xml
new file mode 100644
index 0000000..84b89ca
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="16dp" />
+ <!--By default this outline will not show hence 0 width.
+ width is set programmatically when needed and is gated by the flag:
+ com.android.systemui.Flags.pinInputFieldStyledFocusState-->
+ <stroke android:width="0dp"
+ android:color="@color/bouncer_password_focus_color" />
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
index 66c54f2..0b35559 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
@@ -23,7 +23,7 @@
android:layout_marginTop="@dimen/keyguard_lock_padding"
android:importantForAccessibility="no"
android:ellipsize="marquee"
- android:focusable="true"
+ android:focusable="false"
android:gravity="center"
android:singleLine="true" />
</merge>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 0628c3e..ddad1e3 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -25,6 +25,12 @@
<!-- Maximum width of the sliding KeyguardSecurityContainer -->
<dimen name="keyguard_security_width">420dp</dimen>
+ <!-- Width for the keyguard pin input field -->
+ <dimen name="keyguard_pin_field_width">292dp</dimen>
+
+ <!-- Width for the keyguard pin input field -->
+ <dimen name="keyguard_pin_field_height">48dp</dimen>
+
<!-- Height of the sliding KeyguardSecurityContainer
(includes 2x keyguard_security_view_top_margin) -->
<dimen name="keyguard_security_height">420dp</dimen>
diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index 565ed10..f51e109 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -62,10 +62,10 @@
<string name="keyguard_plugged_in_charging_slowly"><xliff:g id="percentage">%s</xliff:g> • Charging slowly</string>
<!-- When the lock screen is showing and the phone plugged in, and the defend mode is triggered, say that charging is temporarily limited. -->
- <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging optimized to protect battery</string>
+ <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging on hold to protect battery</string>
<!-- When the lock screen is showing and the phone plugged in with incompatible charger. -->
- <string name="keyguard_plugged_in_incompatible_charger"><xliff:g id="percentage">%s</xliff:g> • Issue with charging accessory</string>
+ <string name="keyguard_plugged_in_incompatible_charger"><xliff:g id="percentage">%s</xliff:g> • Check charging accessory</string>
<!-- SIM messages --><skip />
<!-- When the user inserts a sim card from an unsupported network, it becomes network locked -->
diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml
index 2cca951..4789a22 100644
--- a/packages/SystemUI/res-keyguard/values/styles.xml
+++ b/packages/SystemUI/res-keyguard/values/styles.xml
@@ -76,6 +76,7 @@
</style>
<style name="Widget.TextView.Password" parent="@android:style/Widget.TextView">
<item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+ <item name="android:background">@drawable/bouncer_password_text_view_focused_background</item>
<item name="android:gravity">center</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index cfb4017..55606aa 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -86,8 +86,8 @@
<!-- Power Menu Lite -->
<!-- These values are for small screen landscape. For larger landscape screens, they are
overlaid -->
- <dimen name="global_actions_button_size">72dp</dimen>
- <dimen name="global_actions_button_padding">26dp</dimen>
+ <dimen name="global_actions_button_size">64dp</dimen>
+ <dimen name="global_actions_button_padding">20dp</dimen>
<!-- scroll view the size of 2 channel rows -->
<dimen name="notification_blocker_channel_list_height">128dp</dimen>
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index bcc3c83..61a323d4 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -93,6 +93,8 @@
<color name="qs_user_switcher_selected_avatar_icon_color">#202124</color>
<!-- Color of background circle of user avatars in quick settings user switcher -->
<color name="qs_user_switcher_avatar_background">#3C4043</color>
+ <!-- Color of border for keyguard password input when focused -->
+ <color name="bouncer_password_focus_color">@*android:color/system_secondary_dark</color>
<!-- Accessibility floating menu -->
<color name="accessibility_floating_menu_background">#B3000000</color> <!-- 70% -->
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 8be1cc7..3839dd9 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -56,6 +56,8 @@
<color name="kg_user_switcher_restricted_avatar_icon_color">@color/GM2_grey_600</color>
<!-- Color of background circle of user avatars in keyguard user switcher -->
<color name="user_avatar_color_bg">?android:attr/colorBackgroundFloating</color>
+ <!-- Color of border for keyguard password input when focused -->
+ <color name="bouncer_password_focus_color">@*android:color/system_secondary_light</color>
<!-- Icon color for user avatars in user switcher quick settings -->
<color name="qs_user_switcher_avatar_icon_color">#3C4043</color>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index a5a545a..033f93b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -3,6 +3,7 @@
import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN;
import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN;
import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE;
+import static com.android.systemui.Flags.migrateClocksToBlueprint;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -191,11 +192,11 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
-
- mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
- mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
- mStatusArea = findViewById(R.id.keyguard_status_area);
-
+ if (!migrateClocksToBlueprint()) {
+ mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
+ mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
+ mStatusArea = findViewById(R.id.keyguard_status_area);
+ }
onConfigChanged();
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 66f965a..efd8f7f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -37,6 +37,7 @@
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyboard.data.repository.KeyboardRepository;
import com.android.systemui.log.BouncerLogger;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.policy.DevicePostureController;
@@ -210,6 +211,7 @@
private final FeatureFlags mFeatureFlags;
private final SelectedUserInteractor mSelectedUserInteractor;
private final UiEventLogger mUiEventLogger;
+ private final KeyboardRepository mKeyboardRepository;
@Inject
public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -223,7 +225,8 @@
DevicePostureController devicePostureController,
KeyguardViewController keyguardViewController,
FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor,
- UiEventLogger uiEventLogger) {
+ UiEventLogger uiEventLogger,
+ KeyboardRepository keyboardRepository) {
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mLockPatternUtils = lockPatternUtils;
mLatencyTracker = latencyTracker;
@@ -240,6 +243,7 @@
mFeatureFlags = featureFlags;
mSelectedUserInteractor = selectedUserInteractor;
mUiEventLogger = uiEventLogger;
+ mKeyboardRepository = keyboardRepository;
}
/** Create a new {@link KeyguardInputViewController}. */
@@ -268,19 +272,22 @@
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, emergencyButtonController, mFalsingCollector,
mDevicePostureController, mFeatureFlags, mSelectedUserInteractor,
- mUiEventLogger);
+ mUiEventLogger, mKeyboardRepository
+ );
} else if (keyguardInputView instanceof KeyguardSimPinView) {
return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, mTelephonyManager, mFalsingCollector,
- emergencyButtonController, mFeatureFlags, mSelectedUserInteractor);
+ emergencyButtonController, mFeatureFlags, mSelectedUserInteractor,
+ mKeyboardRepository);
} else if (keyguardInputView instanceof KeyguardSimPukView) {
return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, mTelephonyManager, mFalsingCollector,
- emergencyButtonController, mFeatureFlags, mSelectedUserInteractor);
+ emergencyButtonController, mFeatureFlags, mSelectedUserInteractor,
+ mKeyboardRepository);
}
throw new RuntimeException("Unable to find controller for " + keyguardInputView);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
index 36fe75f..fcff0db 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
@@ -25,6 +25,7 @@
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
+import static com.android.systemui.Flags.pinInputFieldStyledFocusState;
import android.animation.Animator;
import android.animation.AnimatorSet;
@@ -168,7 +169,9 @@
// Set selected property on so the view can send accessibility events.
mPasswordEntry.setSelected(true);
- mPasswordEntry.setDefaultFocusHighlightEnabled(false);
+ if (!pinInputFieldStyledFocusState()) {
+ mPasswordEntry.setDefaultFocusHighlightEnabled(false);
+ }
mOkButton = findViewById(R.id.key_enter);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 376933d..60dd568 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -16,17 +16,25 @@
package com.android.keyguard;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+import static com.android.systemui.Flags.pinInputFieldStyledFocusState;
+
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyboard.data.repository.KeyboardRepository;
import com.android.systemui.res.R;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -35,6 +43,7 @@
private final LiftToActivateListener mLiftToActivateListener;
private final FalsingCollector mFalsingCollector;
+ private final KeyboardRepository mKeyboardRepository;
protected PasswordTextView mPasswordEntry;
private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> {
@@ -65,12 +74,14 @@
EmergencyButtonController emergencyButtonController,
FalsingCollector falsingCollector,
FeatureFlags featureFlags,
- SelectedUserInteractor selectedUserInteractor) {
+ SelectedUserInteractor selectedUserInteractor,
+ KeyboardRepository keyboardRepository) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, falsingCollector,
emergencyButtonController, featureFlags, selectedUserInteractor);
mLiftToActivateListener = liftToActivateListener;
mFalsingCollector = falsingCollector;
+ mKeyboardRepository = keyboardRepository;
mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId());
}
@@ -120,6 +131,35 @@
});
okButton.setOnHoverListener(mLiftToActivateListener);
}
+ if (pinInputFieldStyledFocusState()) {
+ collectFlow(mPasswordEntry, mKeyboardRepository.isAnyKeyboardConnected(),
+ this::setKeyboardBasedFocusOutline);
+
+ /**
+ * new UI Specs for PIN Input field have new dimensions go/pin-focus-states.
+ * However we want these changes behind a flag, and resource files cannot be flagged
+ * hence the dimension change in code. When the flags are removed these dimensions
+ * should be set in resources permanently and the code below removed.
+ */
+ ViewGroup.LayoutParams layoutParams = mPasswordEntry.getLayoutParams();
+ layoutParams.width = (int) getResources().getDimension(
+ R.dimen.keyguard_pin_field_width);
+ layoutParams.height = (int) getResources().getDimension(
+ R.dimen.keyguard_pin_field_height);
+ }
+ }
+
+ private void setKeyboardBasedFocusOutline(boolean isAnyKeyboardConnected) {
+ StateListDrawable background = (StateListDrawable) mPasswordEntry.getBackground();
+ GradientDrawable stateDrawable = (GradientDrawable) background.getStateDrawable(0);
+ int color = getResources().getColor(R.color.bouncer_password_focus_color);
+ if (!isAnyKeyboardConnected) {
+ stateDrawable.setStroke(0, color);
+ } else {
+ int strokeWidthInDP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3,
+ getResources().getDisplayMetrics());
+ stateDrawable.setStroke(strokeWidthInDP, color);
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index 2aab1f1..b958f55 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -28,6 +28,7 @@
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.keyboard.data.repository.KeyboardRepository;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -58,12 +59,13 @@
LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener,
EmergencyButtonController emergencyButtonController,
FalsingCollector falsingCollector,
- DevicePostureController postureController,
- FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor,
- UiEventLogger uiEventLogger) {
+ DevicePostureController postureController, FeatureFlags featureFlags,
+ SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger,
+ KeyboardRepository keyboardRepository) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
- emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor);
+ emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
+ keyboardRepository);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mPostureController = postureController;
mLockPatternUtils = lockPatternUtils;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
index 5729119..1cdcbd0 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
@@ -44,6 +44,7 @@
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyboard.data.repository.KeyboardRepository;
import com.android.systemui.res.R;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -93,10 +94,11 @@
LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener,
TelephonyManager telephonyManager, FalsingCollector falsingCollector,
EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags,
- SelectedUserInteractor selectedUserInteractor) {
+ SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
- emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor);
+ emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
+ keyboardRepository);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
index 05fb5fa..f019d61 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
@@ -39,6 +39,7 @@
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyboard.data.repository.KeyboardRepository;
import com.android.systemui.res.R;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -90,10 +91,11 @@
LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener,
TelephonyManager telephonyManager, FalsingCollector falsingCollector,
EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags,
- SelectedUserInteractor selectedUserInteractor) {
+ SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
- emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor);
+ emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
+ keyboardRepository);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index d372f5a..1758831 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -494,7 +494,7 @@
boolean shouldBeCentered,
boolean animate) {
if (migrateClocksToBlueprint()) {
- mKeyguardInteractor.setClockShouldBeCentered(mSplitShadeEnabled && shouldBeCentered);
+ mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
} else {
mKeyguardClockSwitchController.setSplitShadeCentered(
splitShadeEnabled && shouldBeCentered);
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt
index b15aaaf..e88aaf01 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt
@@ -91,7 +91,7 @@
return app
}
- @UsesReflection(KeepTarget(extendsClassConstant = SysUIComponent::class, methodName = "inject"))
+ @UsesReflection(KeepTarget(instanceOfClassConstant = SysUIComponent::class, methodName = "inject"))
override fun instantiateProviderCompat(cl: ClassLoader, className: String): ContentProvider {
val contentProvider = super.instantiateProviderCompat(cl, className)
if (contentProvider is ContextInitializer) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
index 22b02da..16e7f05 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
@@ -20,9 +20,9 @@
import android.content.res.Resources
import android.content.res.Resources.Theme
import android.graphics.Paint
-import android.hardware.biometrics.PromptContentListItem
-import android.hardware.biometrics.PromptContentListItemBulletedText
-import android.hardware.biometrics.PromptContentListItemPlainText
+import android.hardware.biometrics.PromptContentItem
+import android.hardware.biometrics.PromptContentItemBulletedText
+import android.hardware.biometrics.PromptContentItemPlainText
import android.hardware.biometrics.PromptContentView
import android.hardware.biometrics.PromptVerticalListContentView
import android.text.SpannableString
@@ -111,21 +111,21 @@
return inflater.inflate(R.layout.biometric_prompt_content_row_layout, null) as LinearLayout
}
-private fun PromptContentListItem.doesExceedMaxLinesIfTwoColumn(
+private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn(
resources: Resources,
): Boolean {
val passedInText: CharSequence =
when (this) {
- is PromptContentListItemPlainText -> text
- is PromptContentListItemBulletedText -> text
+ is PromptContentItemPlainText -> text
+ is PromptContentItemBulletedText -> text
else -> {
- throw IllegalStateException("No such ListItem: $this")
+ throw IllegalStateException("No such PromptContentItem: $this")
}
}
when (this) {
- is PromptContentListItemPlainText,
- is PromptContentListItemBulletedText -> {
+ is PromptContentItemPlainText,
+ is PromptContentItemBulletedText -> {
val dialogMargin =
resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding)
val halfDialogWidth =
@@ -155,12 +155,12 @@
return numLines > maxLines
}
else -> {
- throw IllegalStateException("No such ListItem: $this")
+ throw IllegalStateException("No such PromptContentItem: $this")
}
}
}
-private fun PromptContentListItem.toView(
+private fun PromptContentItem.toView(
resources: Resources,
inflater: LayoutInflater,
theme: Theme,
@@ -171,10 +171,10 @@
textView.layoutParams = lp
when (this) {
- is PromptContentListItemPlainText -> {
+ is PromptContentItemPlainText -> {
textView.text = text
}
- is PromptContentListItemBulletedText -> {
+ is PromptContentItemBulletedText -> {
val bulletedText = SpannableString(text)
val span =
BulletSpan(
@@ -186,25 +186,25 @@
textView.text = bulletedText
}
else -> {
- throw IllegalStateException("No such ListItem: $this")
+ throw IllegalStateException("No such PromptContentItem: $this")
}
}
return textView
}
-private fun PromptContentListItem.getListItemPadding(resources: Resources): Int {
+private fun PromptContentItem.getListItemPadding(resources: Resources): Int {
var listItemPadding =
resources.getDimensionPixelSize(
R.dimen.biometric_prompt_content_list_item_padding_horizontal
) * 2
when (this) {
- is PromptContentListItemPlainText -> {}
- is PromptContentListItemBulletedText -> {
+ is PromptContentItemPlainText -> {}
+ is PromptContentItemBulletedText -> {
listItemPadding +=
getListItemBulletRadius(resources) * 2 + getListItemBulletGapWidth(resources)
}
else -> {
- throw IllegalStateException("No such ListItem: $this")
+ throw IllegalStateException("No such PromptContentItem: $this")
}
}
return listItemPadding
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
index adbb37c..4184ef7 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
@@ -31,6 +31,7 @@
is PackageChangeModel.UpdateStarted -> "started updating"
is PackageChangeModel.UpdateFinished -> "finished updating"
is PackageChangeModel.Changed -> "changed"
+ is PackageChangeModel.Empty -> throw IllegalStateException("Unexpected empty value: $model")
}
/** A debug logger for [PackageChangeRepository]. */
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
index 853eff7..3ae87c3 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
@@ -23,6 +23,14 @@
val packageName: String
val packageUid: Int
+ /** Empty change, provided for convenience when a sensible default value is needed. */
+ data object Empty : PackageChangeModel {
+ override val packageName: String
+ get() = ""
+ override val packageUid: Int
+ get() = 0
+ }
+
/**
* An existing application package was uninstalled.
*
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 84708a4..4da348e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -24,6 +24,7 @@
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.media.controls.ui.MediaHost
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
@@ -36,6 +37,15 @@
val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene
+ /** Whether widgets are currently being re-ordered. */
+ open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false)
+
+ private val _selectedIndex: MutableStateFlow<Int?> = MutableStateFlow(null)
+
+ /** The index of the currently selected item, or null if no item selected. */
+ val selectedIndex: StateFlow<Int?>
+ get() = _selectedIndex
+
fun onSceneChanged(scene: CommunalSceneKey) {
communalInteractor.onSceneChanged(scene)
}
@@ -105,4 +115,9 @@
/** Called as the user cancels dragging a widget to reorder. */
open fun onReorderWidgetCancel() {}
+
+ /** Set the index of the currently selected item */
+ fun setSelectedIndex(index: Int?) {
+ _selectedIndex.value = index
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 7faf653..fcad45f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -37,7 +37,10 @@
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
/** The view model for communal hub in edit mode. */
@SysUISingleton
@@ -69,9 +72,15 @@
// Only widgets are editable. The CTA tile comes last in the list and remains visible.
override val communalContent: Flow<List<CommunalContentModel>> =
- communalInteractor.widgetContent.map { widgets ->
- widgets + listOf(CommunalContentModel.CtaTileInEditMode())
- }
+ communalInteractor.widgetContent
+ // Clear the selected index when the list is updated.
+ .onEach { setSelectedIndex(null) }
+ .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) }
+
+ private val _reorderingWidgets = MutableStateFlow(false)
+
+ override val reorderingWidgets: StateFlow<Boolean>
+ get() = _reorderingWidgets
override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
@@ -135,14 +144,19 @@
}
override fun onReorderWidgetStart() {
+ // Clear selection status
+ setSelectedIndex(null)
+ _reorderingWidgets.value = true
uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START)
}
override fun onReorderWidgetEnd() {
+ _reorderingWidgets.value = false
uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH)
}
override fun onReorderWidgetCancel() {
+ _reorderingWidgets.value = false
uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index b2d7052..6d9994f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -556,7 +556,7 @@
@Provides
@Singleton
static SubscriptionManager provideSubscriptionManager(Context context) {
- return context.getSystemService(SubscriptionManager.class);
+ return context.getSystemService(SubscriptionManager.class).createForAllUserProfiles();
}
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt
new file mode 100644
index 0000000..afe9151
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import android.view.View
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+@SysUISingleton
+class KeyguardSmartspaceRepository @Inject constructor() {
+ private val _bcSmartspaceVisibility: MutableStateFlow<Int> = MutableStateFlow(View.GONE)
+ val bcSmartspaceVisibility: StateFlow<Int> = _bcSmartspaceVisibility.asStateFlow()
+
+ fun setBcSmartspaceVisibility(visibility: Int) {
+ _bcSmartspaceVisibility.value = visibility
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt
new file mode 100644
index 0000000..67b5745
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardSmartspaceRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+@SysUISingleton
+class KeyguardSmartspaceInteractor
+@Inject
+constructor(private val keyguardSmartspaceRepository: KeyguardSmartspaceRepository) {
+ var bcSmartspaceVisibility: StateFlow<Int> = keyguardSmartspaceRepository.bcSmartspaceVisibility
+
+ fun setBcSmartspaceVisibility(visibility: Int) {
+ keyguardSmartspaceRepository.setBcSmartspaceVisibility(visibility)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index a8223ea..b43ab5e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -37,17 +37,19 @@
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn
/** Encapsulates business-logic related to the keyguard transitions. */
+@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class KeyguardTransitionInteractor
@Inject
@@ -192,29 +194,121 @@
val finishedKeyguardTransitionStep: Flow<TransitionStep> =
repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
- /** The destination state of the last started transition. */
+ /** The destination state of the last [TransitionState.STARTED] transition. */
val startedKeyguardState: SharedFlow<KeyguardState> =
startedKeyguardTransitionStep
.map { step -> step.to }
.shareIn(scope, SharingStarted.Eagerly, replay = 1)
- /** The last completed [KeyguardState] transition */
+ /**
+ * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition.
+ *
+ * WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a
+ * value when a subsequent transition is STARTED. It will *only* emit once we have finally
+ * FINISHED in a state. This can have unintuitive implications.
+ *
+ * For example, if we're transitioning from GONE -> DOZING, and that transition is CANCELED in
+ * favor of a DOZING -> LOCKSCREEN transition, the FINISHED state is still GONE, and will remain
+ * GONE throughout the DOZING -> LOCKSCREEN transition until the DOZING -> LOCKSCREEN transition
+ * finishes (at which point we'll be FINISHED in LOCKSCREEN).
+ *
+ * Since there's no real limit to how many consecutive transitions can be canceled, it's even
+ * possible for the FINISHED state to be the same as the STARTED state while still
+ * transitioning.
+ *
+ * For example:
+ * 1. We're finished in GONE.
+ * 2. The user presses the power button, starting a GONE -> DOZING transition. We're still
+ * FINISHED in GONE.
+ * 3. The user changes their mind, pressing the power button to wake up; this starts a DOZING ->
+ * LOCKSCREEN transition. We're still FINISHED in GONE.
+ * 4. The user quickly swipes away the lockscreen prior to DOZING -> LOCKSCREEN finishing; this
+ * starts a LOCKSCREEN -> GONE transition. We're still FINISHED in GONE, but we've also
+ * STARTED a transition *to* GONE.
+ * 5. We'll emit KeyguardState.GONE again once the transition finishes.
+ *
+ * If you just need to know when we eventually settle into a state, this flow is likely
+ * sufficient. However, if you're having issues with state *during* transitions started after
+ * one or more canceled transitions, you probably need to use [currentKeyguardState].
+ */
val finishedKeyguardState: SharedFlow<KeyguardState> =
finishedKeyguardTransitionStep
.map { step -> step.to }
.shareIn(scope, SharingStarted.Eagerly, replay = 1)
/**
- * Whether we're currently in a transition to a new [KeyguardState] and haven't yet completed
- * it.
+ * The [KeyguardState] we're currently in.
+ *
+ * If we're not in transition, this is simply the [finishedKeyguardState]. If we're in
+ * transition, this is the state we're transitioning *from*.
+ *
+ * Absent CANCELED transitions, [currentKeyguardState] and [finishedKeyguardState] are always
+ * identical - if a transition FINISHES in a given state, the subsequent state we START a
+ * transition *from* would always be that same previously FINISHED state.
+ *
+ * However, if a transition is CANCELED, the next transition will START from a state we never
+ * FINISHED in. For example, if we transition from GONE -> DOZING, but CANCEL that transition in
+ * favor of DOZING -> LOCKSCREEN, we've STARTED a transition *from* DOZING despite never
+ * FINISHING in DOZING. Thus, the current state will be DOZING but the FINISHED state will still
+ * be GONE.
+ *
+ * In this example, if there was DOZING-related state that needs to be set up in order to
+ * properly render a DOZING -> LOCKSCREEN transition, it would never be set up if we were
+ * listening for [finishedKeyguardState] to emit DOZING. However, [currentKeyguardState] would
+ * emit DOZING immediately upon STARTING DOZING -> LOCKSCREEN, allowing us to set up the state.
+ *
+ * Whether you want to use [currentKeyguardState] or [finishedKeyguardState] depends on your
+ * specific use case and how you want to handle cancellations. In general, if you're dealing
+ * with state/UI present across multiple [KeyguardState]s, you probably want
+ * [currentKeyguardState]. If you're dealing with state/UI encapsulated within a single state,
+ * you likely want [finishedKeyguardState].
+ *
+ * As an example, let's say you want to animate in a message on the lockscreen UI after waking
+ * up, and that TextView is not involved in animations between states. You'd want to collect
+ * [finishedKeyguardState], so you'll only animate it in once we're settled on the lockscreen.
+ * If you use [currentKeyguardState] in this case, a DOZING -> LOCKSCREEN transition that is
+ * interrupted by a LOCKSCREEN -> GONE transition would cause the message to become visible
+ * immediately upon LOCKSCREEN -> GONE STARTING, as the current state would become LOCKSCREEN in
+ * that case. That's likely not what you want.
+ *
+ * On the other hand, let's say you're animating the smartspace from alpha 0f to 1f during
+ * DOZING -> LOCKSCREEN, but the transition is interrupted by LOCKSCREEN -> GONE. LS -> GONE
+ * needs the smartspace to be alpha=1f so that it can play the shared-element unlock animation.
+ * In this case, we'd want to collect [currentKeyguardState] and ensure the smartspace is
+ * visible when the current state is LOCKSCREEN. If you use [finishedKeyguardState] in this
+ * case, the smartspace will never be set to alpha = 1f and you'll have a half-faded smartspace
+ * during the LS -> GONE transition.
+ *
+ * If you need special-case handling for cancellations (such as conditional handling depending
+ * on which [KeyguardState] was canceled) you can collect [canceledKeyguardTransitionStep]
+ * directly.
+ *
+ * As a helpful footnote, here's the values of [finishedKeyguardState] and
+ * [currentKeyguardState] during a sequence with two cancellations:
+ * 1. We're FINISHED in GONE. currentKeyguardState=GONE; finishedKeyguardState=GONE.
+ * 2. We START a transition from GONE -> DOZING. currentKeyguardState=GONE;
+ * finishedKeyguardState=GONE.
+ * 3. We CANCEL this transition and START a transition from DOZING -> LOCKSCREEN.
+ * currentKeyguardState=DOZING; finishedKeyguardState=GONE.
+ * 4. We subsequently also CANCEL DOZING -> LOCKSCREEN and START LOCKSCREEN -> GONE.
+ * currentKeyguardState=LOCKSCREEN finishedKeyguardState=GONE.
+ * 5. LOCKSCREEN -> GONE is allowed to FINISH. currentKeyguardState=GONE;
+ * finishedKeyguardState=GONE.
*/
- val isInTransitionToAnyState =
- combine(
- startedKeyguardTransitionStep,
- finishedKeyguardState,
- ) { startedStep, finishedState ->
- startedStep.to != finishedState
- }
+ val currentKeyguardState: SharedFlow<KeyguardState> =
+ repository.transitions
+ .mapLatest {
+ if (it.transitionState == TransitionState.FINISHED) {
+ it.to
+ } else {
+ it.from
+ }
+ }
+ .distinctUntilChanged()
+ .shareIn(scope, SharingStarted.Eagerly, replay = 1)
+
+ /** Whether we've currently STARTED a transition and haven't yet FINISHED it. */
+ val isInTransitionToAnyState = isInTransitionWhere({ true }, { true })
/**
* The amount of transition into or out of the given [KeyguardState].
@@ -304,13 +398,12 @@
fromStatePredicate: (KeyguardState) -> Boolean,
toStatePredicate: (KeyguardState) -> Boolean,
): Flow<Boolean> {
- return combine(
- startedKeyguardTransitionStep,
- finishedKeyguardState,
- ) { startedStep, finishedState ->
- fromStatePredicate(startedStep.from) &&
- toStatePredicate(startedStep.to) &&
- finishedState != startedStep.to
+ return repository.transitions
+ .filter { it.transitionState != TransitionState.CANCELED }
+ .mapLatest {
+ it.transitionState != TransitionState.FINISHED &&
+ fromStatePredicate(it.from) &&
+ toStatePredicate(it.to)
}
.distinctUntilChanged()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index bf763b4..400b8bf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -30,7 +30,10 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
+import com.android.keyguard.KeyguardClockSwitch.LARGE
+import com.android.keyguard.KeyguardClockSwitch.SMALL
import com.android.systemui.Flags.migrateClocksToBlueprint
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
@@ -48,6 +51,7 @@
keyguardRootView: ConstraintLayout,
viewModel: KeyguardClockViewModel,
keyguardClockInteractor: KeyguardClockInteractor,
+ blueprintInteractor: KeyguardBlueprintInteractor,
) {
keyguardRootView.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
@@ -61,18 +65,16 @@
viewModel.currentClock.collect { currentClock ->
cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer)
viewModel.clock = currentClock
- addClockViews(currentClock, keyguardRootView, viewModel.burnInLayer)
- viewModel.burnInLayer?.updatePostLayout(keyguardRootView)
- applyConstraints(clockSection, keyguardRootView, true)
+ addClockViews(currentClock, keyguardRootView)
+ updateBurnInLayer(keyguardRootView, viewModel)
+ blueprintInteractor.refreshBlueprint()
}
}
- // TODO: Weather clock dozing animation
- // will trigger both shouldBeCentered and clockSize change
- // we should avoid this
launch {
if (!migrateClocksToBlueprint()) return@launch
viewModel.clockSize.collect {
- applyConstraints(clockSection, keyguardRootView, true)
+ updateBurnInLayer(keyguardRootView, viewModel)
+ blueprintInteractor.refreshBlueprint()
}
}
launch {
@@ -82,7 +84,7 @@
if (it.largeClock.config.hasCustomPositionUpdatedAnimation) {
playClockCenteringAnimation(clockSection, keyguardRootView, it)
} else {
- applyConstraints(clockSection, keyguardRootView, true)
+ blueprintInteractor.refreshBlueprint()
}
}
}
@@ -90,6 +92,29 @@
}
}
}
+ @VisibleForTesting
+ fun updateBurnInLayer(
+ keyguardRootView: ConstraintLayout,
+ viewModel: KeyguardClockViewModel,
+ ) {
+ val burnInLayer = viewModel.burnInLayer
+ val clockController = viewModel.currentClock.value
+ clockController?.let { clock ->
+ when (viewModel.clockSize.value) {
+ LARGE -> {
+ clock.smallClock.layout.views.forEach { burnInLayer?.removeView(it) }
+ if (clock.config.useAlternateSmartspaceAODTransition) {
+ clock.largeClock.layout.views.forEach { burnInLayer?.addView(it) }
+ }
+ }
+ SMALL -> {
+ clock.smallClock.layout.views.forEach { burnInLayer?.addView(it) }
+ clock.largeClock.layout.views.forEach { burnInLayer?.removeView(it) }
+ }
+ }
+ }
+ viewModel.burnInLayer?.updatePostLayout(keyguardRootView)
+ }
private fun cleanupClockViews(
clockController: ClockController?,
@@ -116,7 +141,6 @@
fun addClockViews(
clockController: ClockController?,
rootView: ConstraintLayout,
- burnInLayer: Layer?
) {
clockController?.let { clock ->
clock.smallClock.layout.views[0].id = R.id.lockscreen_clock_view
@@ -125,17 +149,10 @@
}
// small clock should either be a single view or container with id
// `lockscreen_clock_view`
- clock.smallClock.layout.views.forEach {
- rootView.addView(it)
- burnInLayer?.addView(it)
- }
+ clock.smallClock.layout.views.forEach { rootView.addView(it) }
clock.largeClock.layout.views.forEach { rootView.addView(it) }
- if (clock.config.useAlternateSmartspaceAODTransition) {
- clock.largeClock.layout.views.forEach { burnInLayer?.addView(it) }
- }
}
}
-
fun applyConstraints(
clockSection: ClockSection,
rootView: ConstraintLayout,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index 81ce8f0..10392e3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -16,15 +16,13 @@
package com.android.systemui.keyguard.ui.binder
-import android.transition.TransitionManager
import android.view.View
import androidx.constraintlayout.helper.widget.Layer
import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
@@ -35,10 +33,10 @@
object KeyguardSmartspaceViewBinder {
@JvmStatic
fun bind(
- smartspaceSection: SmartspaceSection,
keyguardRootView: ConstraintLayout,
clockViewModel: KeyguardClockViewModel,
smartspaceViewModel: KeyguardSmartspaceViewModel,
+ blueprintInteractor: KeyguardBlueprintInteractor,
) {
keyguardRootView.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -46,22 +44,56 @@
if (!migrateClocksToBlueprint()) return@launch
clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay
->
- if (hasCustomWeatherDataDisplay) {
- removeDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel)
- } else {
- addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel)
- }
- clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView)
- val constraintSet = ConstraintSet().apply { clone(keyguardRootView) }
- smartspaceSection.applyConstraints(constraintSet)
- TransitionManager.beginDelayedTransition(keyguardRootView)
- constraintSet.applyTo(keyguardRootView)
+ updateDateWeatherToBurnInLayer(
+ keyguardRootView,
+ clockViewModel,
+ smartspaceViewModel
+ )
+ blueprintInteractor.refreshBlueprint()
+ }
+ }
+
+ launch {
+ smartspaceViewModel.bcSmartspaceVisibility.collect {
+ updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel)
+ blueprintInteractor.refreshBlueprint()
}
}
}
}
}
+ private fun updateBCSmartspaceInBurnInLayer(
+ keyguardRootView: ConstraintLayout,
+ clockViewModel: KeyguardClockViewModel,
+ ) {
+ // Visibility is controlled by updateTargetVisibility in CardPagerAdapter
+ val burnInLayer = keyguardRootView.requireViewById<Layer>(R.id.burn_in_layer)
+ burnInLayer.apply {
+ val smartspaceView =
+ keyguardRootView.requireViewById<View>(sharedR.id.bc_smartspace_view)
+ if (smartspaceView.visibility == View.VISIBLE) {
+ addView(smartspaceView)
+ } else {
+ removeView(smartspaceView)
+ }
+ }
+ clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView)
+ }
+
+ private fun updateDateWeatherToBurnInLayer(
+ keyguardRootView: ConstraintLayout,
+ clockViewModel: KeyguardClockViewModel,
+ smartspaceViewModel: KeyguardSmartspaceViewModel
+ ) {
+ if (clockViewModel.hasCustomWeatherDataDisplay.value) {
+ removeDateWeatherFromBurnInLayer(keyguardRootView, smartspaceViewModel)
+ } else {
+ addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel)
+ }
+ clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView)
+ }
+
private fun addDateWeatherToBurnInLayer(
constraintLayout: ConstraintLayout,
smartspaceViewModel: KeyguardSmartspaceViewModel
@@ -76,13 +108,13 @@
constraintLayout.requireViewById<View>(sharedR.id.date_smartspace_view)
val weatherView =
constraintLayout.requireViewById<View>(sharedR.id.weather_smartspace_view)
- addView(weatherView)
addView(dateView)
+ addView(weatherView)
}
}
}
- private fun removeDateWeatherToBurnInLayer(
+ private fun removeDateWeatherFromBurnInLayer(
constraintLayout: ConstraintLayout,
smartspaceViewModel: KeyguardSmartspaceViewModel
) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
index 24d0602..8472a9f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
@@ -23,6 +23,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
+import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection
import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
@@ -30,11 +31,10 @@
import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule
-import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeClockSection
+import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeMediaSection
import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeNotificationStackScrollLayoutSection
-import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeSmartspaceSection
import com.android.systemui.util.kotlin.getOrNull
import java.util.Optional
import javax.inject.Inject
@@ -62,8 +62,8 @@
aodNotificationIconsSection: AodNotificationIconsSection,
aodBurnInSection: AodBurnInSection,
communalTutorialIndicatorSection: CommunalTutorialIndicatorSection,
- smartspaceSection: SplitShadeSmartspaceSection,
- clockSection: SplitShadeClockSection,
+ clockSection: ClockSection,
+ smartspaceSection: SmartspaceSection,
mediaSection: SplitShadeMediaSection,
) : KeyguardBlueprint {
override val id: String = ID
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
index d0626d5..fd530b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
@@ -25,6 +25,8 @@
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.helper.widget.Layer
+import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView
+import com.android.systemui.res.R
class BaseBlueprintTransition : TransitionSet() {
init {
@@ -33,7 +35,16 @@
.addTransition(ChangeBounds())
.addTransition(AlphaInVisibility())
excludeTarget(Layer::class.java, /* exclude= */ true)
+ excludeClockAndSmartspaceViews()
}
+
+ private fun excludeClockAndSmartspaceViews() {
+ excludeTarget(R.id.lockscreen_clock_view, true)
+ excludeTarget(R.id.lockscreen_clock_view_large, true)
+ excludeTarget(SmartspaceView::class.java, true)
+ // TODO(b/319468190): need to exclude views from large weather clock
+ }
+
class AlphaOutVisibility : Visibility() {
override fun onDisappear(
sceneRoot: ViewGroup?,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt
new file mode 100644
index 0000000..67a20e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.view.layout.sections
+
+import android.content.Context
+import android.view.View
+import androidx.constraintlayout.helper.widget.Layer
+
+class AodBurnInLayer(context: Context) : Layer(context) {
+ // For setScale in Layer class, it stores it in mScaleX/Y and directly apply scale to
+ // referenceViews instead of keeping the value in fields of View class
+ // when we try to clone ConstraintSet, it will call getScaleX from View class and return 1.0
+ // and when we clone and apply, it will reset everything in the layer
+ // which cause the flicker from AOD to LS
+ private var _scaleX = 1F
+ private var _scaleY = 1F
+ // As described for _scaleX and _scaleY, we have similar issue with translation
+ private var _translationX = 1F
+ private var _translationY = 1F
+ // avoid adding views with same ids
+ override fun addView(view: View?) {
+ view?.let { if (it.id !in referencedIds) super.addView(view) }
+ }
+ override fun setScaleX(scaleX: Float) {
+ _scaleX = scaleX
+ super.setScaleX(scaleX)
+ }
+
+ override fun getScaleX(): Float {
+ return _scaleX
+ }
+
+ override fun setScaleY(scaleY: Float) {
+ _scaleY = scaleY
+ super.setScaleY(scaleY)
+ }
+
+ override fun getScaleY(): Float {
+ return _scaleY
+ }
+
+ override fun setTranslationX(dx: Float) {
+ _translationX = dx
+ super.setTranslationX(dx)
+ }
+
+ override fun getTranslationX(): Float {
+ return _translationX
+ }
+
+ override fun setTranslationY(dy: Float) {
+ _translationY = dy
+ super.setTranslationY(dy)
+ }
+
+ override fun getTranslationY(): Float {
+ return _translationY
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
index 1ccc6cc..3d36eb0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt
@@ -19,16 +19,13 @@
import android.content.Context
import android.view.View
-import androidx.constraintlayout.helper.widget.Layer
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.res.R
-import com.android.systemui.shared.R as sharedR
import javax.inject.Inject
/** Adds a layer to group elements for translation for burn-in preventation */
@@ -37,10 +34,8 @@
constructor(
private val context: Context,
private val clockViewModel: KeyguardClockViewModel,
- private val smartspaceViewModel: KeyguardSmartspaceViewModel,
) : KeyguardSection() {
- lateinit var burnInLayer: Layer
-
+ private lateinit var burnInLayer: AodBurnInLayer
override fun addViews(constraintLayout: ConstraintLayout) {
if (!KeyguardShadeMigrationNssl.isEnabled) {
return
@@ -48,7 +43,7 @@
val nic = constraintLayout.requireViewById<View>(R.id.aod_notification_icon_container)
burnInLayer =
- Layer(context).apply {
+ AodBurnInLayer(context).apply {
id = R.id.burn_in_layer
addView(nic)
if (!migrateClocksToBlueprint()) {
@@ -57,11 +52,6 @@
addView(statusView)
}
}
- if (migrateClocksToBlueprint()) {
- // weather and date parts won't be added here, cause their visibility doesn't align
- // with others in burnInLayer
- addSmartspaceViews(constraintLayout)
- }
constraintLayout.addView(burnInLayer)
}
@@ -83,14 +73,4 @@
override fun removeViews(constraintLayout: ConstraintLayout) {
constraintLayout.removeView(R.id.burn_in_layer)
}
-
- private fun addSmartspaceViews(constraintLayout: ConstraintLayout) {
- burnInLayer.apply {
- if (smartspaceViewModel.isSmartspaceEnabled) {
- val smartspaceView =
- constraintLayout.requireViewById<View>(sharedR.id.bc_smartspace_view)
- addView(smartspaceView)
- }
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index f560b5f..ed7abff 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -30,9 +30,7 @@
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.keyguard.shared.model.KeyguardSection
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.res.R
-import com.android.systemui.shared.R as sharedR
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
@@ -53,13 +51,13 @@
private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
private val notificationIconAreaController: NotificationIconAreaController,
- private val smartspaceViewModel: KeyguardSmartspaceViewModel,
private val systemBarUtilsState: SystemBarUtilsState,
) : KeyguardSection() {
private var nicBindingDisposable: DisposableHandle? = null
private val nicId = R.id.aod_notification_icon_container
private lateinit var nic: NotificationIconContainer
+ private val smartSpaceBarrier = View.generateViewId()
override fun addViews(constraintLayout: ConstraintLayout) {
if (!KeyguardShadeMigrationNssl.isEnabled) {
@@ -118,7 +116,7 @@
}
constraintSet.apply {
if (migrateClocksToBlueprint()) {
- connect(nicId, TOP, sharedR.id.bc_smartspace_view, BOTTOM, bottomMargin)
+ connect(nicId, TOP, R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin)
setGoneMargin(nicId, BOTTOM, bottomMargin)
} else {
connect(nicId, TOP, R.id.keyguard_status_view, topAlignment, bottomMargin)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index b5f32c8..b344d3b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -23,11 +23,14 @@
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.INVISIBLE
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
+import androidx.constraintlayout.widget.ConstraintSet.VISIBLE
import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
import com.android.systemui.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.binder.KeyguardClockViewBinder
@@ -35,8 +38,10 @@
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockFaceLayout
import com.android.systemui.res.R
+import com.android.systemui.shared.R as sharedR
import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.Utils
+import dagger.Lazy
import javax.inject.Inject
internal fun ConstraintSet.setVisibility(
@@ -56,6 +61,7 @@
protected val keyguardClockViewModel: KeyguardClockViewModel,
private val context: Context,
private val splitShadeStateController: SplitShadeStateController,
+ val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
) : KeyguardSection() {
override fun addViews(constraintLayout: ConstraintLayout) {}
@@ -68,6 +74,7 @@
constraintLayout,
keyguardClockViewModel,
clockInteractor,
+ blueprintInteractor.get()
)
}
@@ -88,12 +95,16 @@
): ConstraintSet {
// Add constraint between rootView and clockContainer
applyDefaultConstraints(constraintSet)
+ getNonTargetClockFace(clock).applyConstraints(constraintSet)
getTargetClockFace(clock).applyConstraints(constraintSet)
// Add constraint between elements in clock and clock container
return constraintSet.apply {
- setAlpha(getTargetClockFace(clock).views, 1F)
- setAlpha(getNonTargetClockFace(clock).views, 0F)
+ setVisibility(getTargetClockFace(clock).views, VISIBLE)
+ setVisibility(getNonTargetClockFace(clock).views, INVISIBLE)
+ if (!keyguardClockViewModel.useLargeClock) {
+ connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM)
+ }
}
}
@@ -107,9 +118,12 @@
private fun getLargeClockFace(clock: ClockController): ClockFaceLayout = clock.largeClock.layout
private fun getSmallClockFace(clock: ClockController): ClockFaceLayout = clock.smallClock.layout
open fun applyDefaultConstraints(constraints: ConstraintSet) {
+ val guideline =
+ if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID
+ else R.id.split_shade_guideline
constraints.apply {
connect(R.id.lockscreen_clock_view_large, START, PARENT_ID, START)
- connect(R.id.lockscreen_clock_view_large, END, PARENT_ID, END)
+ connect(R.id.lockscreen_clock_view_large, END, guideline, END)
connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP)
var largeClockTopMargin =
context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index b0eee0a..8c5e9b4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.ui.view.layout.sections
import android.content.Context
+import android.view.View
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
import androidx.constraintlayout.widget.ConstraintSet.END
@@ -27,11 +28,9 @@
import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.shared.R as sharedR
import com.android.systemui.statusbar.notification.stack.AmbientState
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
@@ -54,7 +53,6 @@
ambientState: AmbientState,
controller: NotificationStackScrollLayoutController,
notificationStackSizeCalculator: NotificationStackSizeCalculator,
- private val smartspaceViewModel: KeyguardSmartspaceViewModel,
@Main mainDispatcher: CoroutineDispatcher,
) :
NotificationStackScrollLayoutSection(
@@ -69,6 +67,7 @@
notificationStackSizeCalculator,
mainDispatcher,
) {
+ private val smartSpaceBarrier = View.generateViewId()
override fun applyConstraints(constraintSet: ConstraintSet) {
if (!KeyguardShadeMigrationNssl.isEnabled) {
return
@@ -76,16 +75,14 @@
constraintSet.apply {
val bottomMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin)
-
if (migrateClocksToBlueprint()) {
connect(
R.id.nssl_placeholder,
TOP,
- sharedR.id.bc_smartspace_view,
+ R.id.smart_space_barrier_bottom,
BOTTOM,
bottomMargin
)
- setGoneMargin(R.id.nssl_placeholder, TOP, bottomMargin)
} else {
connect(R.id.nssl_placeholder, TOP, R.id.keyguard_status_view, BOTTOM, bottomMargin)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
index eacd466..37842a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
@@ -18,37 +18,41 @@
import android.content.Context
import android.view.View
+import android.view.View.GONE
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import androidx.constraintlayout.widget.Barrier
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.END
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import androidx.constraintlayout.widget.ConstraintSet.START
-import androidx.constraintlayout.widget.ConstraintSet.TOP
-import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.binder.KeyguardSmartspaceViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
-import com.android.systemui.res.R
+import com.android.systemui.shared.R
import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
+import dagger.Lazy
import javax.inject.Inject
open class SmartspaceSection
@Inject
constructor(
+ val context: Context,
val keyguardClockViewModel: KeyguardClockViewModel,
val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel,
- private val context: Context,
+ val keyguardSmartspaceInteractor: KeyguardSmartspaceInteractor,
val smartspaceController: LockscreenSmartspaceController,
val keyguardUnlockAnimationController: KeyguardUnlockAnimationController,
+ val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
) : KeyguardSection() {
private var smartspaceView: View? = null
private var weatherView: View? = null
private var dateView: View? = null
+ private var smartspaceVisibilityListener: OnGlobalLayoutListener? = null
+
override fun addViews(constraintLayout: ConstraintLayout) {
if (!migrateClocksToBlueprint()) {
return
@@ -64,6 +68,20 @@
}
}
keyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView
+ smartspaceVisibilityListener =
+ object : OnGlobalLayoutListener {
+ var pastVisibility = GONE
+ override fun onGlobalLayout() {
+ smartspaceView?.let {
+ val newVisibility = it.visibility
+ if (pastVisibility != newVisibility) {
+ keyguardSmartspaceInteractor.setBcSmartspaceVisibility(newVisibility)
+ pastVisibility = newVisibility
+ }
+ }
+ }
+ }
+ smartspaceView?.viewTreeObserver?.addOnGlobalLayoutListener(smartspaceVisibilityListener)
}
override fun bindData(constraintLayout: ConstraintLayout) {
@@ -71,10 +89,10 @@
return
}
KeyguardSmartspaceViewBinder.bind(
- this,
constraintLayout,
keyguardClockViewModel,
keyguardSmartspaceViewModel,
+ blueprintInteractor.get(),
)
}
@@ -82,65 +100,96 @@
if (!migrateClocksToBlueprint()) {
return
}
- // Generally, weather should be next to dateView
- // smartspace should be below date & weather views
constraintSet.apply {
// migrate addDateWeatherView, addWeatherView from KeyguardClockSwitchController
- dateView?.let { dateView ->
- constrainHeight(dateView.id, WRAP_CONTENT)
- constrainWidth(dateView.id, WRAP_CONTENT)
- connect(
- dateView.id,
- START,
- PARENT_ID,
- START,
- context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start)
+ constrainHeight(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ constrainWidth(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ connect(
+ R.id.date_smartspace_view,
+ ConstraintSet.START,
+ ConstraintSet.PARENT_ID,
+ ConstraintSet.START,
+ context.resources.getDimensionPixelSize(
+ com.android.systemui.res.R.dimen.below_clock_padding_start
)
- }
- weatherView?.let {
- constrainWidth(it.id, WRAP_CONTENT)
- dateView?.let { dateView ->
- connect(it.id, TOP, dateView.id, TOP)
- connect(it.id, BOTTOM, dateView.id, BOTTOM)
- connect(it.id, START, dateView.id, END, 4)
- }
- }
+ )
+ constrainWidth(R.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ connect(
+ R.id.weather_smartspace_view,
+ ConstraintSet.TOP,
+ R.id.date_smartspace_view,
+ ConstraintSet.TOP
+ )
+ connect(
+ R.id.weather_smartspace_view,
+ ConstraintSet.BOTTOM,
+ R.id.date_smartspace_view,
+ ConstraintSet.BOTTOM
+ )
+ connect(
+ R.id.weather_smartspace_view,
+ ConstraintSet.START,
+ R.id.date_smartspace_view,
+ ConstraintSet.END,
+ 4
+ )
+
// migrate addSmartspaceView from KeyguardClockSwitchController
- smartspaceView?.let {
- constrainHeight(it.id, WRAP_CONTENT)
+ constrainHeight(R.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ connect(
+ R.id.bc_smartspace_view,
+ ConstraintSet.START,
+ ConstraintSet.PARENT_ID,
+ ConstraintSet.START,
+ context.resources.getDimensionPixelSize(
+ com.android.systemui.res.R.dimen.below_clock_padding_start
+ )
+ )
+ connect(
+ R.id.bc_smartspace_view,
+ ConstraintSet.END,
+ if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID
+ else com.android.systemui.res.R.id.split_shade_guideline,
+ ConstraintSet.END,
+ context.resources.getDimensionPixelSize(
+ com.android.systemui.res.R.dimen.below_clock_padding_end
+ )
+ )
+
+ if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
+ clear(R.id.date_smartspace_view, ConstraintSet.TOP)
connect(
- it.id,
- START,
- PARENT_ID,
- START,
- context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start)
+ R.id.date_smartspace_view,
+ ConstraintSet.BOTTOM,
+ R.id.bc_smartspace_view,
+ ConstraintSet.TOP
+ )
+ } else {
+ clear(R.id.date_smartspace_view, ConstraintSet.BOTTOM)
+ connect(
+ R.id.date_smartspace_view,
+ ConstraintSet.TOP,
+ com.android.systemui.res.R.id.lockscreen_clock_view,
+ ConstraintSet.BOTTOM
)
connect(
- it.id,
- END,
- PARENT_ID,
- END,
- context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_end)
+ R.id.bc_smartspace_view,
+ ConstraintSet.TOP,
+ R.id.date_smartspace_view,
+ ConstraintSet.BOTTOM
)
}
- if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
- dateView?.let { dateView ->
- smartspaceView?.let { smartspaceView ->
- connect(dateView.id, BOTTOM, smartspaceView.id, TOP)
- }
- }
- } else {
- dateView?.let { dateView ->
- clear(dateView.id, BOTTOM)
- connect(dateView.id, TOP, R.id.lockscreen_clock_view, BOTTOM)
- constrainHeight(dateView.id, WRAP_CONTENT)
- smartspaceView?.let { smartspaceView ->
- clear(smartspaceView.id, TOP)
- connect(smartspaceView.id, TOP, dateView.id, BOTTOM)
- }
- }
- }
+ createBarrier(
+ com.android.systemui.res.R.id.smart_space_barrier_bottom,
+ Barrier.BOTTOM,
+ 0,
+ *intArrayOf(
+ R.id.bc_smartspace_view,
+ R.id.date_smartspace_view,
+ R.id.weather_smartspace_view,
+ )
+ )
}
updateVisibility(constraintSet)
}
@@ -156,30 +205,28 @@
}
}
}
+ smartspaceView?.viewTreeObserver?.removeOnGlobalLayoutListener(smartspaceVisibilityListener)
+ smartspaceVisibilityListener = null
}
private fun updateVisibility(constraintSet: ConstraintSet) {
constraintSet.apply {
- weatherView?.let {
- setVisibility(
- it.id,
- when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
- true -> ConstraintSet.GONE
- false ->
- when (keyguardSmartspaceViewModel.isWeatherEnabled) {
- true -> ConstraintSet.VISIBLE
- false -> ConstraintSet.GONE
- }
- }
- )
- }
- dateView?.let {
- setVisibility(
- it.id,
- if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE
- else ConstraintSet.VISIBLE
- )
- }
+ setVisibility(
+ R.id.weather_smartspace_view,
+ when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
+ true -> ConstraintSet.GONE
+ false ->
+ when (keyguardSmartspaceViewModel.isWeatherEnabled) {
+ true -> ConstraintSet.VISIBLE
+ false -> ConstraintSet.GONE
+ }
+ }
+ )
+ setVisibility(
+ R.id.date_smartspace_view,
+ if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE
+ else ConstraintSet.VISIBLE
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt
deleted file mode 100644
index 19ba1aa..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.view.layout.sections
-
-import android.content.Context
-import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.policy.SplitShadeStateController
-import javax.inject.Inject
-
-class SplitShadeClockSection
-@Inject
-constructor(
- clockInteractor: KeyguardClockInteractor,
- keyguardClockViewModel: KeyguardClockViewModel,
- context: Context,
- splitShadeStateController: SplitShadeStateController,
-) : ClockSection(clockInteractor, keyguardClockViewModel, context, splitShadeStateController) {
- override fun applyDefaultConstraints(constraints: ConstraintSet) {
- super.applyDefaultConstraints(constraints)
- val largeClockEndGuideline =
- if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID
- else R.id.split_shade_guideline
- constraints.apply {
- connect(
- R.id.lockscreen_clock_view_large,
- ConstraintSet.END,
- largeClockEndGuideline,
- ConstraintSet.END
- )
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
index f20ab06..b12a8a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt
@@ -20,7 +20,6 @@
import android.view.View
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
-import androidx.constraintlayout.widget.Barrier
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
@@ -31,11 +30,9 @@
import androidx.constraintlayout.widget.ConstraintSet.TOP
import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.keyguard.shared.model.KeyguardSection
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.media.controls.ui.KeyguardMediaController
import com.android.systemui.res.R
import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.shared.R as sharedR
import javax.inject.Inject
/** Aligns media on left side for split shade, below smartspace, date, and weather. */
@@ -44,11 +41,9 @@
constructor(
private val context: Context,
private val notificationPanelView: NotificationPanelView,
- private val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel,
private val keyguardMediaController: KeyguardMediaController
) : KeyguardSection() {
private val mediaContainerId = R.id.status_view_media_container
- private val smartSpaceBarrier = R.id.smart_space_barrier_bottom
override fun addViews(constraintLayout: ConstraintLayout) {
if (!migrateClocksToBlueprint()) {
@@ -85,18 +80,7 @@
constraintSet.apply {
constrainWidth(mediaContainerId, MATCH_CONSTRAINT)
constrainHeight(mediaContainerId, WRAP_CONTENT)
-
- createBarrier(
- smartSpaceBarrier,
- Barrier.BOTTOM,
- 0,
- *intArrayOf(
- sharedR.id.bc_smartspace_view,
- sharedR.id.date_smartspace_view,
- sharedR.id.weather_smartspace_view,
- )
- )
- connect(mediaContainerId, TOP, smartSpaceBarrier, BOTTOM)
+ connect(mediaContainerId, TOP, R.id.smart_space_barrier_bottom, BOTTOM)
connect(mediaContainerId, START, PARENT_ID, START)
connect(mediaContainerId, END, R.id.split_shade_guideline, END)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt
deleted file mode 100644
index 8728ada..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.view.layout.sections
-
-import android.content.Context
-import com.android.systemui.keyguard.KeyguardUnlockAnimationController
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
-import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
-import javax.inject.Inject
-
-/*
- * We need this class for the splitShadeBlueprint so `addViews` and `removeViews` will be called
- * when switching to and from splitShade.
- */
-class SplitShadeSmartspaceSection
-@Inject
-constructor(
- keyguardClockViewModel: KeyguardClockViewModel,
- keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel,
- context: Context,
- smartspaceController: LockscreenSmartspaceController,
- keyguardUnlockAnimationController: KeyguardUnlockAnimationController,
-) :
- SmartspaceSection(
- keyguardClockViewModel,
- keyguardSmartspaceViewModel,
- context,
- smartspaceController,
- keyguardUnlockAnimationController,
- )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 5bb2782..f37d9f8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -99,34 +99,4 @@
context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
Utils.getStatusBarHeaderHeightKeyguard(context)
}
-
- fun getLargeClockTopMargin(context: Context): Int {
- var largeClockTopMargin =
- context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
- context.resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.small_clock_padding_top
- ) +
- context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
- largeClockTopMargin += getDimen(context, DATE_WEATHER_VIEW_HEIGHT)
- largeClockTopMargin += getDimen(context, ENHANCED_SMARTSPACE_HEIGHT)
- if (!useLargeClock) {
- largeClockTopMargin -=
- context.resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.small_clock_height
- )
- }
-
- return largeClockTopMargin
- }
-
- private fun getDimen(context: Context, name: String): Int {
- val res = context.packageManager.getResourcesForApplication(context.packageName)
- val id = res.getIdentifier(name, "dimen", context.packageName)
- return res.getDimensionPixelSize(id)
- }
-
- companion object {
- private const val DATE_WEATHER_VIEW_HEIGHT = "date_weather_view_height"
- private const val ENHANCED_SMARTSPACE_HEIGHT = "enhanced_smartspace_height"
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
index a1dd720..e8c1ab5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt
@@ -18,6 +18,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor
import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -33,6 +34,7 @@
@Application applicationScope: CoroutineScope,
smartspaceController: LockscreenSmartspaceController,
keyguardClockViewModel: KeyguardClockViewModel,
+ smartspaceInteractor: KeyguardSmartspaceInteractor,
) {
/** Whether the smartspace section is available in the build. */
val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled()
@@ -78,4 +80,7 @@
): Boolean {
return !clockIncludesCustomWeatherDisplay && isWeatherEnabled
}
+
+ /* trigger clock and smartspace constraints change when smartspace appears */
+ var bcSmartspaceVisibility: StateFlow<Int> = smartspaceInteractor.bcSmartspaceVisibility
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 36bbe4e..d792889 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -16,9 +16,13 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.content.res.Resources
+import com.android.keyguard.KeyguardClockSwitch
import com.android.systemui.biometrics.AuthController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.res.R
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
@@ -31,12 +35,28 @@
class LockscreenContentViewModel
@Inject
constructor(
+ clockInteractor: KeyguardClockInteractor,
private val interactor: KeyguardBlueprintInteractor,
private val authController: AuthController,
val longPress: KeyguardLongPressViewModel,
) {
+ private val clockSize = clockInteractor.clockSize
+
val isUdfpsVisible: Boolean
get() = authController.isUdfpsSupported
+ val isLargeClockVisible: Boolean
+ get() = clockSize.value == KeyguardClockSwitch.LARGE
+ val areNotificationsVisible: Boolean
+ get() = !isLargeClockVisible
+
+ fun getSmartSpacePaddingTop(resources: Resources): Int {
+ return if (isLargeClockVisible) {
+ resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
+ resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin)
+ } else {
+ 0
+ }
+ }
fun blueprintId(scope: CoroutineScope): StateFlow<String> {
return interactor.blueprint
diff --git a/packages/SystemUI/src/com/android/systemui/qs/LeftRightArrowPressedListener.kt b/packages/SystemUI/src/com/android/systemui/qs/LeftRightArrowPressedListener.kt
new file mode 100644
index 0000000..ca790e8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/LeftRightArrowPressedListener.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs
+
+import android.view.KeyEvent
+import android.view.View
+import androidx.core.util.Consumer
+
+/**
+ * Listens for left and right arrow keys pressed while focus is on the view.
+ *
+ * Key press is treated as correct when its full lifecycle happened on the view: first
+ * [KeyEvent.ACTION_DOWN] was performed, view didn't lose focus in the meantime and then
+ * [KeyEvent.ACTION_UP] was performed with the same [KeyEvent.getKeyCode]
+ */
+class LeftRightArrowPressedListener private constructor() :
+ View.OnKeyListener, View.OnFocusChangeListener {
+
+ private var lastKeyCode: Int? = 0
+ private var listener: Consumer<Int>? = null
+
+ fun setArrowKeyPressedListener(arrowPressedListener: Consumer<Int>) {
+ listener = arrowPressedListener
+ }
+
+ override fun onKey(view: View, keyCode: Int, keyEvent: KeyEvent): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ // only scroll on ACTION_UP as we don't handle longpressing for now. Still we need
+ // to intercept even ACTION_DOWN otherwise keyboard focus will be moved before we
+ // have a chance to intercept ACTION_UP.
+ if (keyEvent.action == KeyEvent.ACTION_UP && keyCode == lastKeyCode) {
+ listener?.accept(keyCode)
+ lastKeyCode = null
+ } else if (keyEvent.repeatCount == 0) {
+ // we only read key events that are NOT coming from long pressing because that also
+ // causes reading ACTION_DOWN event (with repeated count > 0) when moving focus with
+ // arrow from another sibling view
+ lastKeyCode = keyCode
+ }
+ return true
+ }
+ return false
+ }
+
+ override fun onFocusChange(view: View, hasFocus: Boolean) {
+ // resetting lastKeyCode so we get fresh cleared state on focus
+ if (hasFocus) {
+ lastKeyCode = null
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ fun createAndRegisterListenerForView(view: View): LeftRightArrowPressedListener {
+ val listener = LeftRightArrowPressedListener()
+ view.setOnKeyListener(listener)
+ view.onFocusChangeListener = listener
+ return listener
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
index 4770d52..1c9f5fd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
@@ -1,5 +1,8 @@
package com.android.systemui.qs;
+import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.LEFT;
+import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.RIGHT;
+
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
@@ -9,10 +12,12 @@
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
+import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import com.android.settingslib.Utils;
@@ -43,6 +48,7 @@
private int mPosition = -1;
private boolean mAnimating;
+ private PageScrollActionListener mPageScrollActionListener;
private final Animatable2.AnimationCallback mAnimationCallback =
new Animatable2.AnimationCallback() {
@@ -77,6 +83,14 @@
mPageIndicatorWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_width);
mPageIndicatorHeight = res.getDimensionPixelSize(R.dimen.qs_page_indicator_height);
mPageDotWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_dot_width);
+ LeftRightArrowPressedListener arrowListener =
+ LeftRightArrowPressedListener.createAndRegisterListenerForView(this);
+ arrowListener.setArrowKeyPressedListener(keyCode -> {
+ if (mPageScrollActionListener != null) {
+ int swipeDirection = keyCode == KeyEvent.KEYCODE_DPAD_LEFT ? LEFT : RIGHT;
+ mPageScrollActionListener.onScrollActionTriggered(swipeDirection);
+ }
+ });
}
public void setNumPages(int numPages) {
@@ -280,4 +294,19 @@
getChildAt(i).layout(left, 0, mPageIndicatorWidth + left, mPageIndicatorHeight);
}
}
+
+ void setPageScrollActionListener(PageScrollActionListener listener) {
+ mPageScrollActionListener = listener;
+ }
+
+ interface PageScrollActionListener {
+
+ @IntDef({LEFT, RIGHT})
+ @interface Direction { }
+
+ int LEFT = 0;
+ int RIGHT = 1;
+
+ void onScrollActionTriggered(@Direction int swipeDirection);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 052c0da..43f3a22 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -1,6 +1,8 @@
package com.android.systemui.qs;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE;
+import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.LEFT;
+import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.RIGHT;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -12,7 +14,6 @@
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.AttributeSet;
-import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -30,6 +31,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.plugins.qs.QSTile;
+import com.android.systemui.qs.PageIndicator.PageScrollActionListener.Direction;
import com.android.systemui.qs.QSPanel.QSTileLayout;
import com.android.systemui.qs.QSPanelControllerBase.TileRecord;
import com.android.systemui.qs.logging.QSLogger;
@@ -310,26 +312,18 @@
mPageIndicator = indicator;
mPageIndicator.setNumPages(mPages.size());
mPageIndicator.setLocation(mPageIndicatorPosition);
- mPageIndicator.setOnKeyListener((view, keyCode, keyEvent) -> {
- if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
- // only scroll on ACTION_UP as we don't handle longpressing for now. Still we need
- // to intercept even ACTION_DOWN otherwise keyboard focus will be moved before we
- // have a chance to intercept ACTION_UP.
- if (keyEvent.getAction() == KeyEvent.ACTION_UP && mScroller.isFinished()) {
- scrollByX(getDeltaXForKeyboardScrolling(keyCode),
- SINGLE_PAGE_SCROLL_DURATION_MILLIS);
- }
- return true;
+ mPageIndicator.setPageScrollActionListener(swipeDirection -> {
+ if (mScroller.isFinished()) {
+ scrollByX(getDeltaXForPageScrolling(swipeDirection),
+ SINGLE_PAGE_SCROLL_DURATION_MILLIS);
}
- return false;
});
}
- private int getDeltaXForKeyboardScrolling(int keyCode) {
- if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && getCurrentItem() != 0) {
+ private int getDeltaXForPageScrolling(@Direction int swipeDirection) {
+ if (swipeDirection == LEFT && getCurrentItem() != 0) {
return -getWidth();
- } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
- && getCurrentItem() != mPages.size() - 1) {
+ } else if (swipeDirection == RIGHT && getCurrentItem() != mPages.size() - 1) {
return getWidth();
}
return 0;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
index 6e7e099..bcd09bd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
@@ -18,23 +18,21 @@
import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE
import android.annotation.WorkerThread
-import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
import android.os.UserHandle
import android.service.quicksettings.TileService
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.data.repository.PackageChangeRepository
+import com.android.systemui.common.data.shared.model.PackageChangeModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.kotlin.isComponentActuallyEnabled
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
@@ -52,6 +50,7 @@
constructor(
@Application private val applicationContext: Context,
@Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val packageChangeRepository: PackageChangeRepository
) : InstalledTilesComponentRepository {
override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> {
@@ -70,24 +69,9 @@
)
.packageManager
}
- return conflatedCallbackFlow {
- val receiver =
- object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- trySend(Unit)
- }
- }
- applicationContext.registerReceiverAsUser(
- receiver,
- UserHandle.of(userId),
- INTENT_FILTER,
- /* broadcastPermission = */ null,
- /* scheduler = */ null
- )
-
- awaitClose { applicationContext.unregisterReceiver(receiver) }
- }
- .onStart { emit(Unit) }
+ return packageChangeRepository
+ .packageChanged(UserHandle.of(userId))
+ .onStart { emit(PackageChangeModel.Empty) }
.map { reloadComponents(userId, packageManager) }
.distinctUntilChanged()
.flowOn(backgroundDispatcher)
@@ -104,14 +88,6 @@
}
companion object {
- private val INTENT_FILTER =
- IntentFilter().apply {
- addAction(Intent.ACTION_PACKAGE_ADDED)
- addAction(Intent.ACTION_PACKAGE_CHANGED)
- addAction(Intent.ACTION_PACKAGE_REMOVED)
- addAction(Intent.ACTION_PACKAGE_REPLACED)
- addDataScheme("package")
- }
private val INTENT = Intent(TileService.ACTION_QS_TILE)
private val FLAGS =
ResolveInfoFlags.of(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt
index ec10aaf..88e94e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt
@@ -22,12 +22,17 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import javax.inject.Inject
/** pipeline-agnostic implementation for getting [NotificationVisibility]. */
@SysUISingleton
-class NotificationVisibilityProviderImpl @Inject constructor(
+class NotificationVisibilityProviderImpl
+@Inject
+constructor(
+ private val activeNotificationsInteractor: ActiveNotificationsInteractor,
private val notifDataStore: NotifLiveDataStore,
private val notifCollection: CommonNotifCollection
) : NotificationVisibilityProvider {
@@ -47,5 +52,10 @@
override fun getLocation(key: String): NotificationVisibility.NotificationLocation =
NotificationLogger.getNotificationLocation(notifCollection.getEntry(key))
- private fun getCount() = notifDataStore.activeNotifCount.value
+ private fun getCount() =
+ if (NotificationsLiveDataStoreRefactor.isEnabled) {
+ activeNotificationsInteractor.allNotificationsCountValue
+ } else {
+ notifDataStore.activeNotifCount.value
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index ca6344d..5180bab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -61,6 +61,15 @@
val allRepresentativeNotifications: Flow<Map<String, ActiveNotificationModel>> =
repository.activeNotifications.map { store -> store.individuals }
+ /** Size of the flattened list of Notifications actively presented in the stack. */
+ val allNotificationsCount: Flow<Int> =
+ repository.activeNotifications.map { store -> store.individuals.size }
+
+ /**
+ * The same as [allNotificationsCount], but without flows, for easy access in synchronous code.
+ */
+ val allNotificationsCountValue: Int = repository.activeNotifications.value.individuals.size
+
/** Are any notifications being actively presented in the notification stack? */
val areAnyNotificationsPresent: Flow<Boolean> =
repository.activeNotifications
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt
new file mode 100644
index 0000000..a1fb983
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack
+
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import javax.inject.Inject
+
+/**
+ * Tracks latencies related to temporary hiding notifications while measuring
+ * them, which is an optimization to show some content as early as possible
+ * and perform notifications measurement later.
+ * See [HideNotificationsInteractor].
+ */
+class DisplaySwitchNotificationsHiderTracker @Inject constructor(
+ private val notificationsInteractor: ShadeInteractor,
+ private val latencyTracker: LatencyTracker
+) {
+
+ suspend fun trackNotificationHideTime(shouldHideNotifications: Flow<Boolean>) {
+ shouldHideNotifications
+ .collect { shouldHide ->
+ if (shouldHide) {
+ latencyTracker.onActionStart(ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE)
+ } else {
+ latencyTracker.onActionEnd(ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE)
+ }
+ }
+ }
+
+ suspend fun trackNotificationHideTimeWhenVisible(shouldHideNotifications: Flow<Boolean>) {
+ combine(shouldHideNotifications, notificationsInteractor.isAnyExpanded)
+ { hidden, shadeExpanded -> hidden && shadeExpanded }
+ .distinctUntilChanged()
+ .collect { hiddenButShouldBeVisible ->
+ if (hiddenButShouldBeVisible) {
+ latencyTracker.onActionStart(
+ ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN)
+ } else {
+ latencyTracker.onActionEnd(
+ ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
index 910b40f..c2bc9ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
@@ -16,22 +16,36 @@
package com.android.systemui.statusbar.notification.stack.ui.viewbinder
import androidx.core.view.doOnDetach
+import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
/**
* Binds a [NotificationStackScrollLayoutController] to its [view model][NotificationListViewModel].
*/
object HideNotificationsBinder {
- suspend fun bindHideList(
+ fun CoroutineScope.bindHideList(
viewController: NotificationStackScrollLayoutController,
- viewModel: NotificationListViewModel
+ viewModel: NotificationListViewModel,
+ hiderTracker: DisplaySwitchNotificationsHiderTracker
) {
viewController.view.doOnDetach { viewController.bindHideState(shouldHide = false) }
- viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
- viewController.bindHideState(shouldHide)
+ val hideListFlow = viewModel.hideListViewModel.shouldHideListForPerformance
+ .shareIn(this, started = Lazily)
+
+ launch {
+ hideListFlow.collect { shouldHide ->
+ viewController.bindHideState(shouldHide)
+ }
}
+
+ launch { hiderTracker.trackNotificationHideTime(hideListFlow) }
+ launch { hiderTracker.trackNotificationHideTimeWhenVisible(hideListFlow) }
}
private fun NotificationStackScrollLayoutController.bindHideState(shouldHide: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index da260cb..44a7e7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -35,6 +35,7 @@
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
+import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
@@ -53,6 +54,7 @@
@Inject
constructor(
@Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val hiderTracker: DisplaySwitchNotificationsHiderTracker,
private val configuration: ConfigurationState,
private val falsingManager: FalsingManager,
private val iconAreaController: NotificationIconAreaController,
@@ -74,7 +76,7 @@
view.repeatWhenAttached {
lifecycleScope.launch {
launch { bindShelf(shelf) }
- launch { bindHideList(viewController, viewModel) }
+ bindHideList(viewController, viewModel, hiderTracker)
if (FooterViewRefactor.isEnabled) {
launch { bindFooter(view) }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 5ee38be..a48fb45 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -240,13 +240,13 @@
*/
val translationY: Flow<Float> =
combine(
- isOnLockscreen,
+ isOnLockscreenWithoutShade,
merge(
keyguardInteractor.keyguardTranslationY,
occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
)
- ) { isOnLockscreen, translationY ->
- if (isOnLockscreen) {
+ ) { isOnLockscreenWithoutShade, translationY ->
+ if (isOnLockscreenWithoutShade) {
translationY
} else {
0f
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index 2afb3a1..d86d123 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -640,11 +640,10 @@
@Test
public void deleteWindowMagnification_enabling_expectedValuesAndInvokeCallback()
throws RemoteException {
-
enableWindowMagnificationAndWaitAnimating(mWaitPartialAnimationDuration,
mAnimationCallback);
- Mockito.reset(mSpyController);
+ resetMockObjects();
getInstrumentation().runOnMainSync(() -> {
mWindowMagnificationAnimationController.deleteWindowMagnification(
mAnimationCallback2);
@@ -658,6 +657,11 @@
mValueAnimator.end();
});
+ // wait for animation returns
+ waitForIdleSync();
+
+ // {@link ValueAnimator.AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)} will only
+ // be triggered once in {@link ValueAnimator#end()}
verify(mSpyController).enableWindowMagnificationInternal(
mScaleCaptor.capture(),
mCenterXCaptor.capture(), mCenterYCaptor.capture(),
@@ -717,7 +721,11 @@
deleteWindowMagnificationAndWaitAnimating(mWaitPartialAnimationDuration,
mAnimationCallback);
- deleteWindowMagnificationAndWaitAnimating(0, null);
+ // Verifying that WindowMagnificationController#deleteWindowMagnification is never called
+ // in previous steps
+ verify(mSpyController, never()).deleteWindowMagnification();
+
+ deleteWindowMagnificationWithoutAnimation();
verify(mSpyController).deleteWindowMagnification();
verifyFinalSpec(Float.NaN, Float.NaN, Float.NaN);
@@ -810,6 +818,8 @@
mWindowMagnificationAnimationController.enableWindowMagnification(
targetScale, targetCenterX, targetCenterY, null);
});
+ // wait for animation returns
+ waitForIdleSync();
}
private void enableWindowMagnificationAndWaitAnimating(long duration,
@@ -829,12 +839,16 @@
targetScale, targetCenterX, targetCenterY, callback);
advanceTimeBy(duration);
});
+ // wait for animation returns
+ waitForIdleSync();
}
private void deleteWindowMagnificationWithoutAnimation() {
getInstrumentation().runOnMainSync(() -> {
mWindowMagnificationAnimationController.deleteWindowMagnification(null);
});
+ // wait for animation returns
+ waitForIdleSync();
}
private void deleteWindowMagnificationAndWaitAnimating(long duration,
@@ -843,6 +857,8 @@
mWindowMagnificationAnimationController.deleteWindowMagnification(callback);
advanceTimeBy(duration);
});
+ // wait for animation returns
+ waitForIdleSync();
}
private void verifyStartValue(ArgumentCaptor<Float> captor, float startValue) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
index bd4973d..a57b890 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
@@ -1,6 +1,6 @@
package com.android.systemui.biometrics.domain.model
-import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptContentItemBulletedText
import android.hardware.biometrics.PromptVerticalListContentView
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -28,7 +28,8 @@
val contentView =
PromptVerticalListContentView.Builder()
.setDescription("content description")
- .addListItem(PromptContentListItemBulletedText("content text"))
+ .addListItem(PromptContentItemBulletedText("content item 1"))
+ .addListItem(PromptContentItemBulletedText("content item 2"), 1)
.build()
val fpPros = fingerprintSensorPropertiesInternal().first()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index bf61c2e..3944054 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -18,7 +18,7 @@
import android.content.res.Configuration
import android.graphics.Point
-import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptContentItemBulletedText
import android.hardware.biometrics.PromptContentView
import android.hardware.biometrics.PromptInfo
import android.hardware.biometrics.PromptVerticalListContentView
@@ -140,7 +140,8 @@
selector.resetPrompt()
promptContentView =
PromptVerticalListContentView.Builder()
- .addListItem(PromptContentListItemBulletedText("test"))
+ .addListItem(PromptContentItemBulletedText("content item 1"))
+ .addListItem(PromptContentItemBulletedText("content item 2"), 1)
.build()
viewModel =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
index a4d217f..5dd37ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
@@ -21,13 +21,17 @@
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardClockSwitch.LARGE
+import com.android.keyguard.KeyguardClockSwitch.SMALL
import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.plugins.clocks.ClockConfig
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.plugins.clocks.ClockFaceLayout
import com.android.systemui.util.mockito.whenever
import kotlin.test.Test
+import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.Mock
@@ -48,32 +52,60 @@
@Mock private lateinit var smallClockView: View
@Mock private lateinit var smallClockFaceLayout: ClockFaceLayout
@Mock private lateinit var largeClockFaceLayout: ClockFaceLayout
+ @Mock private lateinit var clockViewModel: KeyguardClockViewModel
+ private val clockSize = MutableStateFlow(LARGE)
+ private val currentClock: MutableStateFlow<ClockController?> = MutableStateFlow(null)
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- }
-
- @Test
- fun addClockViews_nonWeatherClock() {
- setupNonWeatherClock()
- KeyguardClockViewBinder.addClockViews(clock, rootView, burnInLayer)
- verify(rootView).addView(smallClockView)
- verify(rootView).addView(largeClockView)
- verify(burnInLayer).addView(smallClockView)
- verify(burnInLayer, never()).addView(largeClockView)
+ whenever(clockViewModel.clockSize).thenReturn(clockSize)
+ whenever(clockViewModel.currentClock).thenReturn(currentClock)
+ whenever(clockViewModel.burnInLayer).thenReturn(burnInLayer)
}
@Test
fun addClockViews_WeatherClock() {
setupWeatherClock()
- KeyguardClockViewBinder.addClockViews(clock, rootView, burnInLayer)
+ KeyguardClockViewBinder.addClockViews(clock, rootView)
verify(rootView).addView(smallClockView)
verify(rootView).addView(largeClockView)
- verify(burnInLayer).addView(smallClockView)
+ }
+
+ @Test
+ fun addClockViews_nonWeatherClock() {
+ setupNonWeatherClock()
+ KeyguardClockViewBinder.addClockViews(clock, rootView)
+ verify(rootView).addView(smallClockView)
+ verify(rootView).addView(largeClockView)
+ }
+ @Test
+ fun addClockViewsToBurnInLayer_LargeWeatherClock() {
+ setupWeatherClock()
+ clockSize.value = LARGE
+ KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel)
+ verify(burnInLayer).removeView(smallClockView)
verify(burnInLayer).addView(largeClockView)
}
+ @Test
+ fun addClockViewsToBurnInLayer_LargeNonWeatherClock() {
+ setupNonWeatherClock()
+ clockSize.value = LARGE
+ KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel)
+ verify(burnInLayer).removeView(smallClockView)
+ verify(burnInLayer, never()).addView(largeClockView)
+ }
+
+ @Test
+ fun addClockViewsToBurnInLayer_SmallClock() {
+ setupNonWeatherClock()
+ clockSize.value = SMALL
+ KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel)
+ verify(burnInLayer).addView(smallClockView)
+ verify(burnInLayer).removeView(largeClockView)
+ }
+
private fun setupWeatherClock() {
setupClock()
val clockConfig =
@@ -99,5 +131,7 @@
whenever(clock.smallClock).thenReturn(smallClock)
whenever(largeClock.layout).thenReturn(largeClockFaceLayout)
whenever(smallClock.layout).thenReturn(smallClockFaceLayout)
+ whenever(clockViewModel.clock).thenReturn(clock)
+ currentClock.value = clock
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index 070a0cc..57b5559 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -22,6 +22,7 @@
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.res.R
@@ -31,6 +32,8 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
+import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -46,6 +49,8 @@
@Mock private lateinit var keyguardClockInteractor: KeyguardClockInteractor
@Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel
@Mock private lateinit var splitShadeStateController: SplitShadeStateController
+ @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor>
+ private val clockShouldBeCentered: MutableStateFlow<Boolean> = MutableStateFlow(true)
private lateinit var underTest: ClockSection
@@ -104,12 +109,15 @@
whenever(packageManager.getResourcesForApplication(anyString())).thenReturn(remoteResources)
mContext.setMockPackageManager(packageManager)
+ whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered)
+
underTest =
ClockSection(
keyguardClockInteractor,
keyguardClockViewModel,
mContext,
splitShadeStateController,
+ blueprintInteractor
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
index 28da957..deb3a83 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
@@ -26,6 +26,8 @@
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.res.R
@@ -34,7 +36,8 @@
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.StateFlow
+import dagger.Lazy
+import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -50,7 +53,8 @@
@Mock private lateinit var keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel
@Mock private lateinit var lockscreenSmartspaceController: LockscreenSmartspaceController
@Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController
- @Mock private lateinit var hasCustomWeatherDataDisplay: StateFlow<Boolean>
+ @Mock private lateinit var keyguardSmartspaceInteractor: KeyguardSmartspaceInteractor
+ @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor>
private val smartspaceView = View(mContext).also { it.id = sharedR.id.bc_smartspace_view }
private val weatherView = View(mContext).also { it.id = sharedR.id.weather_smartspace_view }
@@ -58,17 +62,22 @@
private lateinit var constraintLayout: ConstraintLayout
private lateinit var constraintSet: ConstraintSet
+ private val clockShouldBeCentered = MutableStateFlow(false)
+ private val hasCustomWeatherDataDisplay = MutableStateFlow(false)
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
underTest =
SmartspaceSection(
+ mContext,
keyguardClockViewModel,
keyguardSmartspaceViewModel,
- mContext,
+ keyguardSmartspaceInteractor,
lockscreenSmartspaceController,
keyguardUnlockAnimationController,
+ blueprintInteractor
)
constraintLayout = ConstraintLayout(mContext)
whenever(lockscreenSmartspaceController.buildAndConnectView(any()))
@@ -78,6 +87,7 @@
whenever(lockscreenSmartspaceController.buildAndConnectDateView(any())).thenReturn(dateView)
whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay)
.thenReturn(hasCustomWeatherDataDisplay)
+ whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered)
constraintSet = ConstraintSet()
}
@@ -115,7 +125,7 @@
fun testConstraintsWhenNotHasCustomWeatherDataDisplay() {
whenever(keyguardSmartspaceViewModel.isSmartspaceEnabled).thenReturn(true)
whenever(keyguardSmartspaceViewModel.isDateWeatherDecoupled).thenReturn(true)
- whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(false)
+ hasCustomWeatherDataDisplay.value = false
underTest.addViews(constraintLayout)
underTest.applyConstraints(constraintSet)
assertWeatherSmartspaceConstrains(constraintSet)
@@ -129,7 +139,7 @@
@Test
fun testConstraintsWhenHasCustomWeatherDataDisplay() {
- whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(true)
+ hasCustomWeatherDataDisplay.value = true
underTest.addViews(constraintLayout)
underTest.applyConstraints(constraintSet)
assertWeatherSmartspaceConstrains(constraintSet)
@@ -140,7 +150,7 @@
@Test
fun testNormalDateWeatherVisibility() {
- whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(false)
+ hasCustomWeatherDataDisplay.value = false
whenever(keyguardSmartspaceViewModel.isWeatherEnabled).thenReturn(true)
underTest.addViews(constraintLayout)
underTest.applyConstraints(constraintSet)
@@ -153,7 +163,7 @@
}
@Test
fun testCustomDateWeatherVisibility() {
- whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(true)
+ hasCustomWeatherDataDisplay.value = true
underTest.addViews(constraintLayout)
underTest.applyConstraints(constraintSet)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt
index 45f0a8c..44c411f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt
@@ -66,6 +66,11 @@
private const val DEFAULT_PACKAGE_NAME = "com.media.projection.test"
private val DEFAULT_USER_HANDLE = UserHandle.getUserHandleForUid(UserHandle.myUserId())
- private val DEFAULT_INFO = MediaProjectionInfo(DEFAULT_PACKAGE_NAME, DEFAULT_USER_HANDLE)
+ private val DEFAULT_INFO =
+ MediaProjectionInfo(
+ DEFAULT_PACKAGE_NAME,
+ DEFAULT_USER_HANDLE,
+ /* launchCookie = */ null
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt
new file mode 100644
index 0000000..60eb3ae
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs
+
+import android.testing.AndroidTestingRunner
+import android.view.KeyEvent
+import android.view.KeyEvent.KEYCODE_DPAD_LEFT
+import android.view.View
+import androidx.core.util.Consumer
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LeftRightArrowPressedListenerTest : SysuiTestCase() {
+
+ private lateinit var underTest: LeftRightArrowPressedListener
+ private val callback =
+ object : Consumer<Int> {
+ var lastValue: Int? = null
+
+ override fun accept(keyCode: Int?) {
+ lastValue = keyCode
+ }
+ }
+
+ private val view = View(context)
+
+ @Before
+ fun setUp() {
+ underTest = LeftRightArrowPressedListener.createAndRegisterListenerForView(view)
+ underTest.setArrowKeyPressedListener(callback)
+ }
+
+ @Test
+ fun shouldTriggerCallback_whenArrowUpReceived_afterArrowDownReceived() {
+ underTest.sendKey(KeyEvent.ACTION_DOWN, KEYCODE_DPAD_LEFT)
+
+ underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT)
+
+ assertThat(callback.lastValue).isEqualTo(KEYCODE_DPAD_LEFT)
+ }
+
+ @Test
+ fun shouldNotTriggerCallback_whenKeyUpReceived_ifKeyDownNotReceived() {
+ underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT)
+
+ assertThat(callback.lastValue).isNull()
+ }
+
+ @Test
+ fun shouldNotTriggerCallback_whenKeyUpReceived_ifKeyDownWasRepeated() {
+ underTest.sendKeyWithRepeat(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT, repeat = 2)
+ underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT)
+
+ assertThat(callback.lastValue).isNull()
+ }
+
+ @Test
+ fun shouldNotTriggerCallback_whenKeyUpReceived_ifKeyDownReceivedBeforeLosingFocus() {
+ underTest.sendKey(KeyEvent.ACTION_DOWN, KEYCODE_DPAD_LEFT)
+ underTest.onFocusChange(view, hasFocus = false)
+ underTest.onFocusChange(view, hasFocus = true)
+
+ underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT)
+
+ assertThat(callback.lastValue).isNull()
+ }
+
+ private fun LeftRightArrowPressedListener.sendKey(action: Int, keyCode: Int) {
+ onKey(view, keyCode, KeyEvent(action, keyCode))
+ }
+
+ private fun LeftRightArrowPressedListener.sendKeyWithRepeat(
+ action: Int,
+ keyCode: Int,
+ repeat: Int
+ ) {
+ val keyEvent =
+ KeyEvent(
+ /* downTime= */ 0L,
+ /* eventTime= */ 0L,
+ /* action= */ action,
+ /* code= */ KEYCODE_DPAD_LEFT,
+ /* repeat= */ repeat
+ )
+ onKey(view, keyCode, keyEvent)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
index db9e548..8ef3f57 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
@@ -2,11 +2,13 @@
import android.content.Context
import android.testing.AndroidTestingRunner
-import android.view.KeyEvent
import android.view.View
import android.widget.Scroller
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.PageIndicator.PageScrollActionListener
+import com.android.systemui.qs.PageIndicator.PageScrollActionListener.LEFT
+import com.android.systemui.qs.PageIndicator.PageScrollActionListener.RIGHT
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -22,7 +24,7 @@
class PagedTileLayoutTest : SysuiTestCase() {
@Mock private lateinit var pageIndicator: PageIndicator
- @Captor private lateinit var captor: ArgumentCaptor<View.OnKeyListener>
+ @Captor private lateinit var captor: ArgumentCaptor<PageScrollActionListener>
private lateinit var pageTileLayout: TestPagedTileLayout
private lateinit var scroller: Scroller
@@ -32,7 +34,7 @@
MockitoAnnotations.initMocks(this)
pageTileLayout = TestPagedTileLayout(mContext)
pageTileLayout.setPageIndicator(pageIndicator)
- verify(pageIndicator).setOnKeyListener(captor.capture())
+ verify(pageIndicator).setPageScrollActionListener(captor.capture())
setViewWidth(pageTileLayout, width = PAGE_WIDTH)
scroller = pageTileLayout.mScroller
}
@@ -43,28 +45,27 @@
}
@Test
- fun scrollsRight_afterRightArrowPressed_whenFocusOnPagerIndicator() {
+ fun scrollsRight_afterRightScrollActionTriggered() {
pageTileLayout.currentPageIndex = 0
- sendUpEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
+ sendScrollActionEvent(RIGHT)
assertThat(scroller.isFinished).isFalse() // aka we're scrolling
assertThat(scroller.finalX).isEqualTo(scroller.currX + PAGE_WIDTH)
}
@Test
- fun scrollsLeft_afterLeftArrowPressed_whenFocusOnPagerIndicator() {
+ fun scrollsLeft_afterLeftScrollActionTriggered() {
pageTileLayout.currentPageIndex = 1 // we won't scroll left if we're on the first page
- sendUpEvent(KeyEvent.KEYCODE_DPAD_LEFT)
+ sendScrollActionEvent(LEFT)
assertThat(scroller.isFinished).isFalse() // aka we're scrolling
assertThat(scroller.finalX).isEqualTo(scroller.currX - PAGE_WIDTH)
}
- private fun sendUpEvent(keyCode: Int) {
- val event = KeyEvent(KeyEvent.ACTION_UP, keyCode)
- captor.value.onKey(pageIndicator, keyCode, event)
+ private fun sendScrollActionEvent(@PageScrollActionListener.Direction direction: Int) {
+ captor.value.onScrollActionTriggered(direction)
}
/**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index b3b10eb..9b641f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -50,6 +50,18 @@
DaggerActiveNotificationsInteractorTest_TestComponent.factory().create(test = this)
@Test
+ fun testAllNotificationsCount() =
+ testComponent.runTest {
+ val count by collectLastValue(underTest.allNotificationsCount)
+
+ activeNotificationListRepository.setActiveNotifs(5)
+ runCurrent()
+
+ assertThat(count).isEqualTo(5)
+ assertThat(underTest.allNotificationsCountValue).isEqualTo(5)
+ }
+
+ @Test
fun testAreAnyNotificationsPresent_isTrue() =
testComponent.runTest {
val areAnyNotificationsPresent by collectLastValue(underTest.areAnyNotificationsPresent)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt
new file mode 100644
index 0000000..1dfcb38
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.stack
+
+import androidx.test.filters.SmallTest
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class DisplaySwitchNotificationsHiderTrackerTest : SysuiTestCase() {
+
+ private val testScope = TestScope()
+ private val shadeInteractor = mock<ShadeInteractor>()
+ private val latencyTracker = mock<LatencyTracker>()
+
+ private val shouldHideFlow = MutableStateFlow(false)
+ private val shadeExpandedFlow = MutableStateFlow(false)
+
+ private val tracker = DisplaySwitchNotificationsHiderTracker(shadeInteractor, latencyTracker)
+
+ @Before
+ fun setup() {
+ whenever(shadeInteractor.isAnyExpanded).thenReturn(shadeExpandedFlow)
+ }
+
+ @Test
+ fun notificationsBecomeHidden_tracksHideActionStart() = testScope.runTest {
+ startTracking()
+
+ shouldHideFlow.value = true
+ runCurrent()
+
+ verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_ACTION)
+ }
+
+ @Test
+ fun notificationsBecomeVisibleAfterHidden_tracksHideActionEnd() = testScope.runTest {
+ startTracking()
+
+ shouldHideFlow.value = true
+ runCurrent()
+ clearInvocations(latencyTracker)
+ shouldHideFlow.value = false
+ runCurrent()
+
+ verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_ACTION)
+ }
+
+ @Test
+ fun notificationsBecomeHiddenWhenShadeIsClosed_doesNotTrackHideWhenVisibleActionStart() =
+ testScope.runTest {
+ shouldHideFlow.value = false
+ shadeExpandedFlow.value = false
+ startTracking()
+
+ shouldHideFlow.value = true
+ runCurrent()
+
+ verify(latencyTracker, never())
+ .onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+ }
+
+ @Test
+ fun notificationsBecomeHiddenWhenShadeIsOpen_tracksHideWhenVisibleActionStart() = testScope.runTest {
+ shouldHideFlow.value = false
+ shadeExpandedFlow.value = false
+ startTracking()
+
+ shouldHideFlow.value = true
+ shadeExpandedFlow.value = true
+ runCurrent()
+
+ verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+ }
+
+ @Test
+ fun shadeBecomesOpenWhenNotificationsHidden_tracksHideWhenVisibleActionStart() =
+ testScope.runTest {
+ shouldHideFlow.value = true
+ shadeExpandedFlow.value = false
+ startTracking()
+
+ shadeExpandedFlow.value = true
+ runCurrent()
+
+ verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+ }
+
+ @Test
+ fun notificationsBecomeVisibleWhenShadeIsOpen_tracksHideWhenVisibleActionEnd() = testScope.runTest {
+ shouldHideFlow.value = false
+ shadeExpandedFlow.value = false
+ startTracking()
+ shouldHideFlow.value = true
+ shadeExpandedFlow.value = true
+ runCurrent()
+ clearInvocations(latencyTracker)
+
+ shouldHideFlow.value = false
+ runCurrent()
+
+ verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+ }
+
+ @Test
+ fun shadeBecomesClosedWhenNotificationsHidden_tracksHideWhenVisibleActionEnd() = testScope.runTest {
+ shouldHideFlow.value = false
+ shadeExpandedFlow.value = false
+ startTracking()
+ shouldHideFlow.value = true
+ shadeExpandedFlow.value = true
+ runCurrent()
+ clearInvocations(latencyTracker)
+
+ shadeExpandedFlow.value = false
+ runCurrent()
+
+ verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+ }
+
+ private fun TestScope.startTracking() {
+ backgroundScope.launch { tracker.trackNotificationHideTime(shouldHideFlow) }
+ backgroundScope.launch { tracker.trackNotificationHideTimeWhenVisible(shouldHideFlow) }
+ runCurrent()
+ clearInvocations(latencyTracker)
+ }
+
+ private companion object {
+ const val HIDE_NOTIFICATIONS_ACTION = ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE
+ const val HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION =
+ ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 9f15b05..a824bc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -484,6 +484,46 @@
}
@Test
+ fun translationYUpdatesOnKeyguard() =
+ testScope.runTest {
+ val translationY by collectLastValue(underTest.translationY)
+
+ configurationRepository.setDimensionPixelSize(
+ R.dimen.keyguard_translate_distance_on_swipe_up,
+ -100
+ )
+ configurationRepository.onAnyConfigurationChange()
+
+ // legacy expansion means the user is swiping up, usually for the bouncer
+ shadeRepository.setLegacyShadeExpansion(0.5f)
+
+ showLockscreen()
+
+ // The translation values are negative
+ assertThat(translationY).isLessThan(0f)
+ }
+
+ @Test
+ fun translationYDoesNotUpdateWhenShadeIsExpanded() =
+ testScope.runTest {
+ val translationY by collectLastValue(underTest.translationY)
+
+ configurationRepository.setDimensionPixelSize(
+ R.dimen.keyguard_translate_distance_on_swipe_up,
+ -100
+ )
+ configurationRepository.onAnyConfigurationChange()
+
+ // legacy expansion means the user is swiping up, usually for the bouncer but also for
+ // shade collapsing
+ shadeRepository.setLegacyShadeExpansion(0.5f)
+
+ showLockscreenWithShadeExpanded()
+
+ assertThat(translationY).isEqualTo(0f)
+ }
+
+ @Test
fun updateBounds_fromKeyguardRoot() =
testScope.runTest {
val bounds by collectLastValue(underTest.bounds)
diff --git a/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt
index a11bf6a..d095f42 100644
--- a/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt
@@ -18,6 +18,11 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import com.android.systemui.util.mockito.mock
var Kosmos.accessibilityManager by Fixture { mock<AccessibilityManager>() }
+
+var Kosmos.accessibilityManagerWrapper by Fixture {
+ AccessibilityManagerWrapper(accessibilityManager)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
index eedc0b9..22dff0a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
@@ -17,8 +17,11 @@
package com.android.internal.logging
import com.android.internal.logging.testing.FakeMetricsLogger
+import com.android.internal.util.LatencyTracker
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
val Kosmos.fakeMetricsLogger by Fixture { FakeMetricsLogger() }
val Kosmos.metricsLogger by Fixture<MetricsLogger> { fakeMetricsLogger }
+val Kosmos.latencyTracker by Fixture { mock<LatencyTracker>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
new file mode 100644
index 0000000..19cd950
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
+import com.android.systemui.keyguard.shared.model.KeyguardSection
+import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.keyguardBlueprintRepository by
+ Kosmos.Fixture {
+ KeyguardBlueprintRepository(
+ configurationRepository = configurationRepository,
+ blueprints = setOf(defaultBlueprint),
+ )
+ }
+
+private val defaultBlueprint =
+ object : KeyguardBlueprint {
+ override val id: String
+ get() = DEFAULT
+ override val sections: List<KeyguardSection>
+ get() = listOf()
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
new file mode 100644
index 0000000..d9a3192
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.splitShadeStateController
+
+val Kosmos.keyguardBlueprintInteractor by
+ Kosmos.Fixture {
+ KeyguardBlueprintInteractor(
+ keyguardBlueprintRepository = keyguardBlueprintRepository,
+ applicationScope = applicationCoroutineScope,
+ context = applicationContext,
+ splitShadeStateController = splitShadeStateController,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
new file mode 100644
index 0000000..638a6a3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.applicationContext
+import android.view.accessibility.accessibilityManagerWrapper
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.uiEventLogger
+
+val Kosmos.keyguardLongPressInteractor by
+ Kosmos.Fixture {
+ KeyguardLongPressInteractor(
+ appContext = applicationContext,
+ scope = applicationCoroutineScope,
+ transitionInteractor = keyguardTransitionInteractor,
+ repository = keyguardRepository,
+ logger = uiEventLogger,
+ featureFlags = featureFlagsClassic,
+ broadcastDispatcher = broadcastDispatcher,
+ accessibilityManager = accessibilityManagerWrapper,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt
new file mode 100644
index 0000000..3c9846a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.keyguardLongPressInteractor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.keyguardLongPressViewModel by
+ Kosmos.Fixture {
+ KeyguardLongPressViewModel(
+ interactor = keyguardLongPressInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
new file mode 100644
index 0000000..96de4ba
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.biometrics.authController
+import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.lockscreenContentViewModel by
+ Kosmos.Fixture {
+ LockscreenContentViewModel(
+ clockInteractor = keyguardClockInteractor,
+ interactor = keyguardBlueprintInteractor,
+ authController = authController,
+ longPress = keyguardLongPressViewModel,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt
new file mode 100644
index 0000000..b12f5af
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack
+
+import com.android.internal.logging.latencyTracker
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+
+val Kosmos.displaySwitchNotificationsHiderTracker by Fixture {
+ DisplaySwitchNotificationsHiderTracker(shadeInteractor, latencyTracker)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index c6498e4..748d04d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -24,6 +24,7 @@
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder
import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger
+import com.android.systemui.statusbar.notification.stack.displaySwitchNotificationsHiderTracker
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
import com.android.systemui.statusbar.phone.notificationIconAreaController
import java.util.Optional
@@ -36,6 +37,7 @@
falsingManager = falsingManager,
iconAreaController = notificationIconAreaController,
metricsLogger = metricsLogger,
+ hiderTracker = displaySwitchNotificationsHiderTracker,
nicBinder = notificationIconContainerShelfViewBinder,
loggerOptional = Optional.of(notificationStatsLogger),
)
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index a856f42..f3b74ea 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -1735,6 +1735,7 @@
processResponseLockedForPcc(response, response.getClientState(), requestFlags);
mFillResponseEventLogger.maybeSetLatencyResponseProcessingMillis();
+ mFillResponseEventLogger.logAndEndEvent();
}
@@ -1847,6 +1848,33 @@
return;
}
synchronized (mLock) {
+ // TODO(b/319913595): refactor logging for fill response for primary and secondary
+ // providers
+ // Start a new FillResponse logger for the success case.
+ mFillResponseEventLogger.startLogForNewResponse();
+ mFillResponseEventLogger.maybeSetRequestId(fillResponse.getRequestId());
+ mFillResponseEventLogger.maybeSetAppPackageUid(uid);
+ mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_SUCCESS);
+ mFillResponseEventLogger.startResponseProcessingTime();
+ // Time passed since session was created
+ final long fillRequestReceivedRelativeTimestamp =
+ SystemClock.elapsedRealtime() - mLatencyBaseTime;
+ mPresentationStatsEventLogger.maybeSetFillResponseReceivedTimestampMs(
+ (int) (fillRequestReceivedRelativeTimestamp));
+ mFillResponseEventLogger.maybeSetLatencyFillResponseReceivedMillis(
+ (int) (fillRequestReceivedRelativeTimestamp));
+ if (mDestroyed) {
+ Slog.w(TAG, "Call to Session#onSecondaryFillResponse() rejected - session: "
+ + id + " destroyed");
+ mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_SESSION_DESTROYED);
+ mFillResponseEventLogger.logAndEndEvent();
+ return;
+ }
+
+ List<Dataset> datasetList = fillResponse.getDatasets();
+ int datasetCount = (datasetList == null) ? 0 : datasetList.size();
+ mFillResponseEventLogger.maybeSetTotalDatasetsProvided(datasetCount);
+ mFillResponseEventLogger.maybeSetAvailableCount(datasetCount);
if (mSecondaryResponses == null) {
mSecondaryResponses = new SparseArray<>(2);
}
@@ -1859,6 +1887,8 @@
if (currentView != null) {
currentView.maybeCallOnFillReady(flags);
}
+ mFillResponseEventLogger.maybeSetLatencyResponseProcessingMillis();
+ mFillResponseEventLogger.logAndEndEvent();
}
}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 056ec89..50e1862 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -21,6 +21,7 @@
import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES;
import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
+import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION;
import static android.Manifest.permission.USE_COMPANION_TRANSPORTS;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
@@ -82,6 +83,7 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.UserInfo;
+import android.hardware.power.Mode;
import android.net.MacAddress;
import android.net.NetworkPolicyManager;
import android.os.Binder;
@@ -90,6 +92,7 @@
import android.os.Message;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
+import android.os.PowerManagerInternal;
import android.os.PowerWhitelistManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
@@ -175,6 +178,7 @@
private final PowerWhitelistManager mPowerWhitelistManager;
private final UserManager mUserManager;
final PackageManagerInternal mPackageManagerInternal;
+ private final PowerManagerInternal mPowerManagerInternal;
/**
* A structure that consists of two nested maps, and effectively maps (userId + packageName) to
@@ -235,6 +239,7 @@
mOnPackageVisibilityChangeListener =
new OnPackageVisibilityChangeListener(mActivityManager);
+ mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
}
@Override
@@ -949,6 +954,10 @@
mAssociationStore.updateAssociation(association);
mDevicePresenceMonitor.onSelfManagedDeviceConnected(associationId);
+
+ if (association.getDeviceProfile() == REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION) {
+ mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, true);
+ }
}
@Override
@@ -963,6 +972,10 @@
}
mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId);
+
+ if (association.getDeviceProfile() == REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION) {
+ mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false);
+ }
}
@Override
diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java
index a0301a9..6e906eb 100644
--- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java
+++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java
@@ -34,7 +34,7 @@
private volatile boolean mShouldProcessRequests = false;
- private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(100);
+ private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(500);
SecureTransport(int associationId, ParcelFileDescriptor fd, Context context) {
super(associationId, fd, context);
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index d1274d4..3b9d92d 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -30,6 +30,8 @@
import android.hardware.input.VirtualMouseButtonEvent;
import android.hardware.input.VirtualMouseRelativeEvent;
import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualStylusButtonEvent;
+import android.hardware.input.VirtualStylusMotionEvent;
import android.hardware.input.VirtualTouchEvent;
import android.os.Handler;
import android.os.IBinder;
@@ -71,12 +73,14 @@
static final String PHYS_TYPE_MOUSE = "Mouse";
static final String PHYS_TYPE_TOUCHSCREEN = "Touchscreen";
static final String PHYS_TYPE_NAVIGATION_TOUCHPAD = "NavigationTouchpad";
+ static final String PHYS_TYPE_STYLUS = "Stylus";
@StringDef(prefix = { "PHYS_TYPE_" }, value = {
PHYS_TYPE_DPAD,
PHYS_TYPE_KEYBOARD,
PHYS_TYPE_MOUSE,
PHYS_TYPE_TOUCHSCREEN,
PHYS_TYPE_NAVIGATION_TOUCHPAD,
+ PHYS_TYPE_STYLUS,
})
@Retention(RetentionPolicy.SOURCE)
@interface PhysType {
@@ -188,6 +192,16 @@
}
}
+ void createStylus(@NonNull String deviceName, int vendorId, int productId,
+ @NonNull IBinder deviceToken, int displayId, int height, int width)
+ throws DeviceCreationException {
+ final String phys = createPhys(PHYS_TYPE_STYLUS);
+ createDeviceInternal(InputDeviceDescriptor.TYPE_STYLUS, deviceName, vendorId,
+ productId, deviceToken, displayId, phys,
+ () -> mNativeWrapper.openUinputStylus(deviceName, vendorId, productId, phys,
+ height, width));
+ }
+
void unregisterInputDevice(@NonNull IBinder token) {
synchronized (mLock) {
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.remove(
@@ -410,6 +424,32 @@
}
}
+ boolean sendStylusMotionEvent(@NonNull IBinder token, @NonNull VirtualStylusMotionEvent event) {
+ synchronized (mLock) {
+ final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
+ token);
+ if (inputDeviceDescriptor == null) {
+ return false;
+ }
+ return mNativeWrapper.writeStylusMotionEvent(inputDeviceDescriptor.getNativePointer(),
+ event.getToolType(), event.getAction(), event.getX(), event.getY(),
+ event.getPressure(), event.getTiltX(), event.getTiltY(),
+ event.getEventTimeNanos());
+ }
+ }
+
+ boolean sendStylusButtonEvent(@NonNull IBinder token, @NonNull VirtualStylusButtonEvent event) {
+ synchronized (mLock) {
+ final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
+ token);
+ if (inputDeviceDescriptor == null) {
+ return false;
+ }
+ return mNativeWrapper.writeStylusButtonEvent(inputDeviceDescriptor.getNativePointer(),
+ event.getButtonCode(), event.getAction(), event.getEventTimeNanos());
+ }
+ }
+
public void dump(@NonNull PrintWriter fout) {
fout.println(" InputController: ");
synchronized (mLock) {
@@ -437,7 +477,7 @@
}
}
- @VisibleForTesting
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Map<IBinder, InputDeviceDescriptor> getInputDeviceDescriptors() {
final Map<IBinder, InputDeviceDescriptor> inputDeviceDescriptors = new ArrayMap<>();
synchronized (mLock) {
@@ -454,6 +494,8 @@
String phys);
private static native long nativeOpenUinputTouchscreen(String deviceName, int vendorId,
int productId, String phys, int height, int width);
+ private static native long nativeOpenUinputStylus(String deviceName, int vendorId,
+ int productId, String phys, int height, int width);
private static native void nativeCloseUinput(long ptr);
private static native boolean nativeWriteDpadKeyEvent(long ptr, int androidKeyCode, int action,
long eventTimeNanos);
@@ -468,6 +510,10 @@
float relativeY, long eventTimeNanos);
private static native boolean nativeWriteScrollEvent(long ptr, float xAxisMovement,
float yAxisMovement, long eventTimeNanos);
+ private static native boolean nativeWriteStylusMotionEvent(long ptr, int toolType, int action,
+ int locationX, int locationY, int pressure, int tiltX, int tiltY, long eventTimeNanos);
+ private static native boolean nativeWriteStylusButtonEvent(long ptr, int buttonCode, int action,
+ long eventTimeNanos);
/** Wrapper around the static native methods for tests. */
@VisibleForTesting
@@ -491,6 +537,11 @@
width);
}
+ public long openUinputStylus(String deviceName, int vendorId, int productId, String phys,
+ int height, int width) {
+ return nativeOpenUinputStylus(deviceName, vendorId, productId, phys, height, width);
+ }
+
public void closeUinput(long ptr) {
nativeCloseUinput(ptr);
}
@@ -527,21 +578,35 @@
long eventTimeNanos) {
return nativeWriteScrollEvent(ptr, xAxisMovement, yAxisMovement, eventTimeNanos);
}
+
+ public boolean writeStylusMotionEvent(long ptr, int toolType, int action, int locationX,
+ int locationY, int pressure, int tiltX, int tiltY, long eventTimeNanos) {
+ return nativeWriteStylusMotionEvent(ptr, toolType, action, locationX, locationY,
+ pressure, tiltX, tiltY, eventTimeNanos);
+ }
+
+ public boolean writeStylusButtonEvent(long ptr, int buttonCode, int action,
+ long eventTimeNanos) {
+ return nativeWriteStylusButtonEvent(ptr, buttonCode, action, eventTimeNanos);
+ }
}
- @VisibleForTesting static final class InputDeviceDescriptor {
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ static final class InputDeviceDescriptor {
static final int TYPE_KEYBOARD = 1;
static final int TYPE_MOUSE = 2;
static final int TYPE_TOUCHSCREEN = 3;
static final int TYPE_DPAD = 4;
static final int TYPE_NAVIGATION_TOUCHPAD = 5;
+ static final int TYPE_STYLUS = 6;
@IntDef(prefix = { "TYPE_" }, value = {
TYPE_KEYBOARD,
TYPE_MOUSE,
TYPE_TOUCHSCREEN,
TYPE_DPAD,
TYPE_NAVIGATION_TOUCHPAD,
+ TYPE_STYLUS,
})
@Retention(RetentionPolicy.SOURCE)
@interface Type {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 44c3a8d..f13f49a 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -23,6 +23,7 @@
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
@@ -75,6 +76,9 @@
import android.hardware.input.VirtualMouseRelativeEvent;
import android.hardware.input.VirtualMouseScrollEvent;
import android.hardware.input.VirtualNavigationTouchpadConfig;
+import android.hardware.input.VirtualStylusButtonEvent;
+import android.hardware.input.VirtualStylusConfig;
+import android.hardware.input.VirtualStylusMotionEvent;
import android.hardware.input.VirtualTouchEvent;
import android.hardware.input.VirtualTouchscreenConfig;
import android.os.Binder;
@@ -258,7 +262,9 @@
runningAppsChangedCallback,
params,
DisplayManagerGlobal.getInstance(),
- Flags.virtualCamera() ? new VirtualCameraController() : null);
+ Flags.virtualCamera()
+ ? new VirtualCameraController(params.getDevicePolicy(POLICY_TYPE_CAMERA))
+ : null);
}
@VisibleForTesting
@@ -776,6 +782,26 @@
@Override // Binder call
@EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ public void createVirtualStylus(@NonNull VirtualStylusConfig config,
+ @NonNull IBinder deviceToken) {
+ super.createVirtualStylus_enforcePermission();
+ Objects.requireNonNull(config);
+ Objects.requireNonNull(deviceToken);
+ checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mInputController.createStylus(config.getInputDeviceName(), config.getVendorId(),
+ config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
+ config.getHeight(), config.getWidth());
+ } catch (InputController.DeviceCreationException e) {
+ throw new IllegalArgumentException(e);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterInputDevice(IBinder token) {
super.unregisterInputDevice_enforcePermission();
final long ident = Binder.clearCallingIdentity();
@@ -881,6 +907,36 @@
@Override // Binder call
@EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ public boolean sendStylusMotionEvent(@NonNull IBinder token,
+ @NonNull VirtualStylusMotionEvent event) {
+ super.sendStylusMotionEvent_enforcePermission();
+ Objects.requireNonNull(token);
+ Objects.requireNonNull(event);
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return mInputController.sendStylusMotionEvent(token, event);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ public boolean sendStylusButtonEvent(@NonNull IBinder token,
+ @NonNull VirtualStylusButtonEvent event) {
+ super.sendStylusButtonEvent_enforcePermission();
+ Objects.requireNonNull(token);
+ Objects.requireNonNull(event);
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return mInputController.sendStylusButtonEvent(token, event);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override // Binder call
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setShowPointerIcon(boolean showPointerIcon) {
super.setShowPointerIcon_enforcePermission();
final long ident = Binder.clearCallingIdentity();
@@ -1337,6 +1393,11 @@
}
}
+ boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId) {
+ return mInputController.getInputDeviceDescriptors().values().stream().anyMatch(
+ inputDeviceDescriptor -> inputDeviceDescriptor.getInputDeviceId() == inputDeviceId);
+ }
+
void onEnteringPipBlocked(int uid) {
// Do nothing. ActivityRecord#checkEnterPictureInPictureState logs that the display does not
// support PiP.
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 0d5cdcb..ef61498 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -838,10 +838,11 @@
}
@Override
- public boolean isDisplayOwnedByAnyVirtualDevice(int displayId) {
+ public boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId) {
ArrayList<VirtualDeviceImpl> virtualDevicesSnapshot = getVirtualDevicesSnapshot();
for (int i = 0; i < virtualDevicesSnapshot.size(); i++) {
- if (virtualDevicesSnapshot.get(i).isDisplayOwnedByVirtualDevice(displayId)) {
+ if (virtualDevicesSnapshot.get(i)
+ .isInputDeviceOwnedByVirtualDevice(inputDeviceId)) {
return true;
}
}
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
index 2f9b6a5..2d82b5e 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 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.
@@ -16,10 +16,13 @@
package com.android.server.companion.virtual.camera;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+
import static com.android.server.companion.virtual.camera.VirtualCameraConversionUtil.getServiceCameraConfiguration;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.companion.virtual.VirtualDeviceParams.DevicePolicy;
import android.companion.virtual.camera.VirtualCameraConfig;
import android.companion.virtualcamera.IVirtualCameraService;
import android.companion.virtualcamera.VirtualCameraConfiguration;
@@ -51,15 +54,21 @@
@GuardedBy("mServiceLock")
@Nullable private IVirtualCameraService mVirtualCameraService;
+ @DevicePolicy
+ private final int mCameraPolicy;
@GuardedBy("mCameras")
private final Map<IBinder, CameraDescriptor> mCameras = new ArrayMap<>();
- public VirtualCameraController() {}
+ public VirtualCameraController(@DevicePolicy int cameraPolicy) {
+ this(/* virtualCameraService= */ null, cameraPolicy);
+ }
@VisibleForTesting
- VirtualCameraController(IVirtualCameraService virtualCameraService) {
+ VirtualCameraController(IVirtualCameraService virtualCameraService,
+ @DevicePolicy int cameraPolicy) {
mVirtualCameraService = virtualCameraService;
+ mCameraPolicy = cameraPolicy;
}
/**
@@ -68,6 +77,8 @@
* @param cameraConfig The {@link VirtualCameraConfig} sent by the client.
*/
public void registerCamera(@NonNull VirtualCameraConfig cameraConfig) {
+ checkConfigByPolicy(cameraConfig);
+
connectVirtualCameraServiceIfNeeded();
try {
@@ -173,6 +184,29 @@
}
}
+ private void checkConfigByPolicy(VirtualCameraConfig config) {
+ if (mCameraPolicy == DEVICE_POLICY_DEFAULT) {
+ throw new IllegalArgumentException(
+ "Cannot create virtual camera with DEVICE_POLICY_DEFAULT for "
+ + "POLICY_TYPE_CAMERA");
+ } else if (isLensFacingAlreadyPresent(config.getLensFacing())) {
+ throw new IllegalArgumentException(
+ "Only a single virtual camera can be created with lens facing "
+ + config.getLensFacing());
+ }
+ }
+
+ private boolean isLensFacingAlreadyPresent(int lensFacing) {
+ synchronized (mCameras) {
+ for (CameraDescriptor cameraDescriptor : mCameras.values()) {
+ if (cameraDescriptor.mConfig.getLensFacing() == lensFacing) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
private void connectVirtualCameraServiceIfNeeded() {
synchronized (mServiceLock) {
// Try to connect to service if not connected already.
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
index f24c4cc..c4a84b0 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
@@ -25,6 +25,7 @@
import android.companion.virtualcamera.SupportedStreamConfiguration;
import android.companion.virtualcamera.VirtualCameraConfiguration;
import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
import android.os.RemoteException;
import android.view.Surface;
@@ -45,12 +46,12 @@
getServiceCameraConfiguration(@NonNull VirtualCameraConfig cameraConfig)
throws RemoteException {
VirtualCameraConfiguration serviceConfiguration = new VirtualCameraConfiguration();
-
serviceConfiguration.supportedStreamConfigs =
cameraConfig.getStreamConfigs().stream()
.map(VirtualCameraConversionUtil::convertSupportedStreamConfiguration)
.toArray(SupportedStreamConfiguration[]::new);
-
+ serviceConfiguration.sensorOrientation = cameraConfig.getSensorOrientation();
+ serviceConfiguration.lensFacing = cameraConfig.getLensFacing();
serviceConfiguration.virtualCameraCallback = convertCallback(cameraConfig.getCallback());
return serviceConfiguration;
}
@@ -60,12 +61,10 @@
@NonNull IVirtualCameraCallback camera) {
return new android.companion.virtualcamera.IVirtualCameraCallback.Stub() {
@Override
- public void onStreamConfigured(
- int streamId, Surface surface, int width, int height, int pixelFormat)
- throws RemoteException {
- VirtualCameraStreamConfig streamConfig =
- createStreamConfig(width, height, pixelFormat);
- camera.onStreamConfigured(streamId, surface, streamConfig);
+ public void onStreamConfigured(int streamId, Surface surface, int width, int height,
+ int format) throws RemoteException {
+ camera.onStreamConfigured(streamId, surface, width, height,
+ convertToJavaFormat(format));
}
@Override
@@ -81,23 +80,30 @@
}
@NonNull
- private static VirtualCameraStreamConfig createStreamConfig(
- int width, int height, int pixelFormat) {
- return new VirtualCameraStreamConfig(width, height, pixelFormat);
- }
-
- @NonNull
private static SupportedStreamConfiguration convertSupportedStreamConfiguration(
VirtualCameraStreamConfig stream) {
SupportedStreamConfiguration supportedConfig = new SupportedStreamConfiguration();
supportedConfig.height = stream.getHeight();
supportedConfig.width = stream.getWidth();
- supportedConfig.pixelFormat = convertFormat(stream.getFormat());
+ supportedConfig.pixelFormat = convertToHalFormat(stream.getFormat());
+ supportedConfig.maxFps = stream.getMaximumFramesPerSecond();
return supportedConfig;
}
- private static int convertFormat(int format) {
- return format == ImageFormat.YUV_420_888 ? Format.YUV_420_888 : Format.UNKNOWN;
+ private static int convertToHalFormat(int javaFormat) {
+ return switch (javaFormat) {
+ case ImageFormat.YUV_420_888 -> Format.YUV_420_888;
+ case PixelFormat.RGBA_8888 -> Format.RGBA_8888;
+ default -> Format.UNKNOWN;
+ };
+ }
+
+ private static int convertToJavaFormat(int halFormat) {
+ return switch (halFormat) {
+ case Format.YUV_420_888 -> ImageFormat.YUV_420_888;
+ case Format.RGBA_8888 -> PixelFormat.RGBA_8888;
+ default -> ImageFormat.UNKNOWN;
+ };
}
private VirtualCameraConversionUtil() {
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 7a4ac6a..2b35231 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -5040,9 +5040,9 @@
@Override
public IFsveritySetupAuthToken createFsveritySetupAuthToken(ParcelFileDescriptor authFd,
- int appUid, @UserIdInt int userId) throws IOException {
+ int uid) throws IOException {
try {
- return mInstaller.createFsveritySetupAuthToken(authFd, appUid, userId);
+ return mInstaller.createFsveritySetupAuthToken(authFd, uid);
} catch (Installer.InstallerException e) {
throw new IOException(e);
}
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index eb6fdd7..f921b0b 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -418,6 +418,8 @@
LinkCapacityEstimate.INVALID, LinkCapacityEstimate.INVALID)));
private List<List<LinkCapacityEstimate>> mLinkCapacityEstimateLists;
+ private int[] mSimultaneousCellularCallingSubIds = {};
+
private int[] mECBMReason;
private boolean[] mECBMStarted;
private int[] mSCBMReason;
@@ -564,7 +566,9 @@
|| events.contains(TelephonyCallback.EVENT_VOICE_ACTIVATION_STATE_CHANGED)
|| events.contains(TelephonyCallback.EVENT_RADIO_POWER_STATE_CHANGED)
|| events.contains(TelephonyCallback.EVENT_ALLOWED_NETWORK_TYPE_LIST_CHANGED)
- || events.contains(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED);
+ || events.contains(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED)
+ || events.contains(TelephonyCallback
+ .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED);
}
private static final int MSG_USER_SWITCHED = 1;
@@ -1427,6 +1431,15 @@
remove(r.binder);
}
}
+ if (events.contains(TelephonyCallback
+ .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED)) {
+ try {
+ r.callback.onSimultaneousCallingStateChanged(
+ mSimultaneousCellularCallingSubIds);
+ } catch (RemoteException ex) {
+ remove(r.binder);
+ }
+ }
if (events.contains(
TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED)) {
try {
@@ -3092,6 +3105,43 @@
}
}
+ /**
+ * Notify the listeners that simultaneous cellular calling subscriptions have changed
+ * @param subIds The set of subIds that support simultaneous cellular calling
+ */
+ public void notifySimultaneousCellularCallingSubscriptionsChanged(int[] subIds) {
+ if (!checkNotifyPermission("notifySimultaneousCellularCallingSubscriptionsChanged()")) {
+ return;
+ }
+
+ if (VDBG) {
+ StringBuilder b = new StringBuilder();
+ b.append("notifySimultaneousCellularCallingSubscriptionsChanged: ");
+ b.append("subIds = {");
+ for (int i : subIds) {
+ b.append(" ");
+ b.append(i);
+ }
+ b.append("}");
+ log(b.toString());
+ }
+
+ synchronized (mRecords) {
+ mSimultaneousCellularCallingSubIds = subIds;
+ for (Record r : mRecords) {
+ if (r.matchTelephonyCallbackEvent(TelephonyCallback
+ .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED)) {
+ try {
+ r.callback.onSimultaneousCallingStateChanged(subIds);
+ } catch (RemoteException ex) {
+ mRemoveList.add(r.binder);
+ }
+ }
+ }
+ handleRemoveListLocked();
+ }
+ }
+
@Override
public void addCarrierPrivilegesCallback(
int phoneId,
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index fd17261..c18bacb 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -172,6 +172,7 @@
public static final String[] AIDL_INTERFACE_PREFIXES_OF_INTEREST = new String[] {
"android.hardware.audio.core.IModule/",
"android.hardware.audio.core.IConfig/",
+ "android.hardware.audio.effect.IFactory/",
"android.hardware.biometrics.face.IFace/",
"android.hardware.biometrics.fingerprint.IFingerprint/",
"android.hardware.bluetooth.IBluetoothHci/",
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 0cff8b7..979449f 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -94,6 +94,8 @@
import static android.os.Process.SHELL_UID;
import static android.os.Process.SYSTEM_UID;
import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
+import static android.content.flags.Flags.enableBindPackageIsolatedProcess;
+
import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICE_BG_LAUNCH;
import static com.android.internal.util.FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_DELEGATE;
@@ -791,13 +793,15 @@
static String getProcessNameForService(ServiceInfo sInfo, ComponentName name,
String callingPackage, String instanceName, boolean isSdkSandbox,
- boolean inSharedIsolatedProcess) {
+ boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess) {
if (isSdkSandbox) {
// For SDK sandbox, the process name is passed in as the instanceName
return instanceName;
}
- if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) {
- // For regular processes, just the name in sInfo
+ if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0
+ || (inPrivateSharedIsolatedProcess && !isDefaultProcessService(sInfo))) {
+ // For regular processes, or private package-shared isolated processes, just the name
+ // in sInfo
return sInfo.processName;
}
// Isolated processes remain.
@@ -809,6 +813,10 @@
}
}
+ private static boolean isDefaultProcessService(ServiceInfo serviceInfo) {
+ return serviceInfo.applicationInfo.processName.equals(serviceInfo.processName);
+ }
+
private static void traceInstant(@NonNull String message, @NonNull ServiceRecord service) {
if (!Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
return;
@@ -864,7 +872,7 @@
ServiceLookupResult res = retrieveServiceLocked(service, instanceName, isSdkSandboxService,
sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, callingPackage,
- callingPid, callingUid, userId, true, callerFg, false, false, null, false);
+ callingPid, callingUid, userId, true, callerFg, false, false, null, false, false);
if (res == null) {
return null;
}
@@ -1550,7 +1558,7 @@
ServiceLookupResult r = retrieveServiceLocked(service, instanceName, isSdkSandboxService,
sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, null,
Binder.getCallingPid(), Binder.getCallingUid(), userId, false, false, false, false,
- null, false);
+ null, false, false);
if (r != null) {
if (r.record != null) {
final long origId = Binder.clearCallingIdentity();
@@ -1642,7 +1650,7 @@
IBinder peekServiceLocked(Intent service, String resolvedType, String callingPackage) {
ServiceLookupResult r = retrieveServiceLocked(service, null, resolvedType, callingPackage,
Binder.getCallingPid(), Binder.getCallingUid(),
- UserHandle.getCallingUserId(), false, false, false, false, false);
+ UserHandle.getCallingUserId(), false, false, false, false, false, false);
IBinder ret = null;
if (r != null) {
@@ -3714,6 +3722,9 @@
|| (flags & Context.BIND_EXTERNAL_SERVICE_LONG) != 0;
final boolean allowInstant = (flags & Context.BIND_ALLOW_INSTANT) != 0;
final boolean inSharedIsolatedProcess = (flags & Context.BIND_SHARED_ISOLATED_PROCESS) != 0;
+ final boolean inPrivateSharedIsolatedProcess =
+ ((flags & Context.BIND_PACKAGE_ISOLATED_PROCESS) != 0)
+ && enableBindPackageIsolatedProcess();
final boolean matchQuarantined =
(flags & Context.BIND_MATCH_QUARANTINED_COMPONENTS) != 0;
@@ -3725,7 +3736,7 @@
isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage,
resolvedType, callingPackage, callingPid, callingUid, userId, true, callerFg,
isBindExternal, allowInstant, null /* fgsDelegateOptions */,
- inSharedIsolatedProcess, matchQuarantined);
+ inSharedIsolatedProcess, inPrivateSharedIsolatedProcess, matchQuarantined);
if (res == null) {
return 0;
}
@@ -4204,14 +4215,14 @@
}
private ServiceLookupResult retrieveServiceLocked(Intent service,
- String instanceName, String resolvedType, String callingPackage,
- int callingPid, int callingUid, int userId,
- boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
- boolean allowInstant, boolean inSharedIsolatedProcess) {
+ String instanceName, String resolvedType, String callingPackage, int callingPid,
+ int callingUid, int userId, boolean createIfNeeded, boolean callingFromFg,
+ boolean isBindExternal, boolean allowInstant, boolean inSharedIsolatedProcess,
+ boolean inPrivateSharedIsolatedProcess) {
return retrieveServiceLocked(service, instanceName, false, INVALID_UID, null, resolvedType,
callingPackage, callingPid, callingUid, userId, createIfNeeded, callingFromFg,
isBindExternal, allowInstant, null /* fgsDelegateOptions */,
- inSharedIsolatedProcess);
+ inSharedIsolatedProcess, inPrivateSharedIsolatedProcess);
}
// TODO(b/265746493): Special case for HotwordDetectionService,
@@ -4233,21 +4244,22 @@
String callingPackage, int callingPid, int callingUid, int userId,
boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
boolean allowInstant, ForegroundServiceDelegationOptions fgsDelegateOptions,
- boolean inSharedIsolatedProcess) {
+ boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess) {
return retrieveServiceLocked(service, instanceName, isSdkSandboxService,
sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, callingPackage,
callingPid, callingUid, userId, createIfNeeded, callingFromFg, isBindExternal,
allowInstant, fgsDelegateOptions, inSharedIsolatedProcess,
- false /* matchQuarantined */);
+ inPrivateSharedIsolatedProcess, false /* matchQuarantined */);
}
- private ServiceLookupResult retrieveServiceLocked(Intent service,
- String instanceName, boolean isSdkSandboxService, int sdkSandboxClientAppUid,
- String sdkSandboxClientAppPackage, String resolvedType,
+ private ServiceLookupResult retrieveServiceLocked(
+ Intent service, String instanceName, boolean isSdkSandboxService,
+ int sdkSandboxClientAppUid, String sdkSandboxClientAppPackage, String resolvedType,
String callingPackage, int callingPid, int callingUid, int userId,
boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
boolean allowInstant, ForegroundServiceDelegationOptions fgsDelegateOptions,
- boolean inSharedIsolatedProcess, boolean matchQuarantined) {
+ boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess,
+ boolean matchQuarantined) {
if (isSdkSandboxService && instanceName == null) {
throw new IllegalArgumentException("No instanceName provided for sdk sandbox process");
}
@@ -4344,7 +4356,8 @@
final ServiceRestarter res = new ServiceRestarter();
final String processName = getProcessNameForService(sInfo, cn, callingPackage,
null /* instanceName */, false /* isSdkSandbox */,
- false /* inSharedIsolatedProcess */);
+ false /* inSharedIsolatedProcess */,
+ false /*inPrivateSharedIsolatedProcess*/);
r = new ServiceRecord(mAm, cn /* name */, cn /* instanceName */,
sInfo.applicationInfo.packageName, sInfo.applicationInfo.uid, filter, sInfo,
callingFromFg, res, processName,
@@ -4415,6 +4428,10 @@
throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
+ className + " is not exported");
}
+ if (inPrivateSharedIsolatedProcess) {
+ throw new SecurityException("BIND_PACKAGE_ISOLATED_PROCESS cannot be "
+ + "applied to an external service.");
+ }
if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) {
throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
+ className + " is not an isolatedProcess");
@@ -4448,28 +4465,32 @@
throw new SecurityException("BIND_EXTERNAL_SERVICE failed, " + name +
" is not an externalService");
}
- if (inSharedIsolatedProcess) {
+ if (inSharedIsolatedProcess && inPrivateSharedIsolatedProcess) {
+ throw new SecurityException("Either BIND_SHARED_ISOLATED_PROCESS or "
+ + "BIND_PACKAGE_ISOLATED_PROCESS should be set. Not both.");
+ }
+ if (inSharedIsolatedProcess || inPrivateSharedIsolatedProcess) {
if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) {
throw new SecurityException("BIND_SHARED_ISOLATED_PROCESS failed, "
+ className + " is not an isolatedProcess");
}
+ }
+ if (inPrivateSharedIsolatedProcess && isDefaultProcessService(sInfo)) {
+ throw new SecurityException("BIND_PACKAGE_ISOLATED_PROCESS cannot be used for "
+ + "services running in the main app process.");
+ }
+ if (inSharedIsolatedProcess) {
+ if (instanceName == null) {
+ throw new IllegalArgumentException("instanceName must be provided for "
+ + "binding a service into a shared isolated process.");
+ }
if ((sInfo.flags & ServiceInfo.FLAG_ALLOW_SHARED_ISOLATED_PROCESS) == 0) {
throw new SecurityException("BIND_SHARED_ISOLATED_PROCESS failed, "
+ className + " has not set the allowSharedIsolatedProcess "
+ " attribute.");
}
- if (instanceName == null) {
- throw new IllegalArgumentException("instanceName must be provided for "
- + "binding a service into a shared isolated process.");
- }
}
if (userId > 0) {
- if (mAm.isSystemUserOnly(sInfo.flags)) {
- Slog.w(TAG_SERVICE, service + " is only available for the SYSTEM user,"
- + " calling userId is: " + userId);
- return null;
- }
-
if (mAm.isSingleton(sInfo.processName, sInfo.applicationInfo,
sInfo.name, sInfo.flags)
&& mAm.isValidSingletonCall(callingUid, sInfo.applicationInfo.uid)) {
@@ -4503,11 +4524,13 @@
= new Intent.FilterComparison(service.cloneFilter());
final ServiceRestarter res = new ServiceRestarter();
String processName = getProcessNameForService(sInfo, name, callingPackage,
- instanceName, isSdkSandboxService, inSharedIsolatedProcess);
+ instanceName, isSdkSandboxService, inSharedIsolatedProcess,
+ inPrivateSharedIsolatedProcess);
r = new ServiceRecord(mAm, className, name, definingPackageName,
definingUid, filter, sInfo, callingFromFg, res,
processName, sdkSandboxClientAppUid,
- sdkSandboxClientAppPackage, inSharedIsolatedProcess);
+ sdkSandboxClientAppPackage,
+ (inSharedIsolatedProcess || inPrivateSharedIsolatedProcess));
res.setService(r);
smap.mServicesByInstanceName.put(name, r);
smap.mServicesByIntent.put(filter, r);
@@ -8504,7 +8527,8 @@
null /* sdkSandboxClientAppPackage */, null /* resolvedType */, callingPackage,
callingPid, callingUid, userId, true /* createIfNeeded */,
false /* callingFromFg */, false /* isBindExternal */, false /* allowInstant */ ,
- options, false /* inSharedIsolatedProcess */);
+ options, false /* inSharedIsolatedProcess */,
+ false /*inPrivateSharedIsolatedProcess*/);
if (res == null || res.record == null) {
Slog.d(TAG,
"startForegroundServiceDelegateLocked retrieveServiceLocked returns null");
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e583a6c..f6d954a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4830,7 +4830,11 @@
if (!mConstants.mEnableWaitForFinishAttachApplication) {
finishAttachApplicationInner(startSeq, callingUid, pid);
}
- maybeSendBootCompletedLocked(app);
+
+ // Temporarily disable sending BOOT_COMPLETED to see if this was impacting perf tests
+ if (false) {
+ maybeSendBootCompletedLocked(app);
+ }
} catch (Exception e) {
// We need kill the process group here. (b/148588589)
Slog.wtf(TAG, "Exception thrown during bind of " + app, e);
@@ -6525,7 +6529,24 @@
@Override
public int checkUriPermission(Uri uri, int pid, int uid,
final int modeFlags, int userId, IBinder callerToken) {
- enforceNotIsolatedCaller("checkUriPermission");
+ return checkUriPermission(uri, pid, uid, modeFlags, userId,
+ /* isFullAccessForContentUri */ false, "checkUriPermission");
+ }
+
+ /**
+ * @param uri This uri must NOT contain an embedded userId.
+ * @param userId The userId in which the uri is to be resolved.
+ */
+ @Override
+ public int checkContentUriPermissionFull(Uri uri, int pid, int uid,
+ final int modeFlags, int userId) {
+ return checkUriPermission(uri, pid, uid, modeFlags, userId,
+ /* isFullAccessForContentUri */ true, "checkContentUriPermissionFull");
+ }
+
+ private int checkUriPermission(Uri uri, int pid, int uid,
+ final int modeFlags, int userId, boolean isFullAccessForContentUri, String methodName) {
+ enforceNotIsolatedCaller(methodName);
// Our own process gets to do everything.
if (pid == MY_PID) {
@@ -6536,8 +6557,10 @@
return PackageManager.PERMISSION_DENIED;
}
}
- return mUgmInternal.checkUriPermission(new GrantUri(userId, uri, modeFlags), uid, modeFlags)
- ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED;
+ boolean granted = mUgmInternal.checkUriPermission(new GrantUri(userId, uri, modeFlags), uid,
+ modeFlags, isFullAccessForContentUri);
+
+ return granted ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED;
}
@Override
@@ -9858,7 +9881,7 @@
@Override
- public void setApplicationStartInfoCompleteListener(
+ public void addApplicationStartInfoCompleteListener(
IApplicationStartInfoCompleteListener listener, int userId) {
enforceNotIsolatedCaller("setApplicationStartInfoCompleteListener");
@@ -9873,7 +9896,8 @@
@Override
- public void clearApplicationStartInfoCompleteListener(int userId) {
+ public void removeApplicationStartInfoCompleteListener(
+ IApplicationStartInfoCompleteListener listener, int userId) {
enforceNotIsolatedCaller("clearApplicationStartInfoCompleteListener");
// For the simplification, we don't support USER_ALL nor USER_CURRENT here.
@@ -9882,7 +9906,8 @@
}
final int callingUid = Binder.getCallingUid();
- mProcessList.getAppStartInfoTracker().clearStartInfoCompleteListener(callingUid, true);
+ mProcessList.getAppStartInfoTracker().removeStartInfoCompleteListener(listener, callingUid,
+ true);
}
@Override
@@ -13747,11 +13772,6 @@
return result;
}
- boolean isSystemUserOnly(int flags) {
- return android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
- && (flags & ServiceInfo.FLAG_SYSTEM_USER_ONLY) != 0;
- }
-
/**
* Checks to see if the caller is in the same app as the singleton
* component, or the component is in a special app. It allows special apps
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 82e554e..b90e5cd 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -119,7 +119,7 @@
/** UID as key. */
@GuardedBy("mLock")
- private final SparseArray<ApplicationStartInfoCompleteCallback> mCallbacks;
+ private final SparseArray<ArrayList<ApplicationStartInfoCompleteCallback>> mCallbacks;
/**
* Whether or not we've loaded the historical app process start info from persistent storage.
@@ -465,11 +465,18 @@
synchronized (mLock) {
if (startInfo.getStartupState()
== ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN) {
- ApplicationStartInfoCompleteCallback callback =
+ final List<ApplicationStartInfoCompleteCallback> callbacks =
mCallbacks.get(startInfo.getRealUid());
- if (callback != null) {
- callback.onApplicationStartInfoComplete(startInfo);
+ if (callbacks == null) {
+ return;
}
+ final int size = callbacks.size();
+ for (int i = 0; i < size; i++) {
+ if (callbacks.get(i) != null) {
+ callbacks.get(i).onApplicationStartInfoComplete(startInfo);
+ }
+ }
+ mCallbacks.remove(startInfo.getRealUid());
}
}
}
@@ -542,7 +549,6 @@
} catch (RemoteException e) {
/*ignored*/
}
- clearStartInfoCompleteListener(mUid, true);
}
void unlinkToDeath() {
@@ -551,7 +557,7 @@
@Override
public void binderDied() {
- clearStartInfoCompleteListener(mUid, false);
+ removeStartInfoCompleteListener(mCallback, mUid, false);
}
}
@@ -561,22 +567,43 @@
if (!mEnabled) {
return;
}
- mCallbacks.put(uid, new ApplicationStartInfoCompleteCallback(listener, uid));
+ ArrayList<ApplicationStartInfoCompleteCallback> callbacks = mCallbacks.get(uid);
+ if (callbacks == null) {
+ mCallbacks.set(uid,
+ callbacks = new ArrayList<ApplicationStartInfoCompleteCallback>());
+ }
+ callbacks.add(new ApplicationStartInfoCompleteCallback(listener, uid));
}
}
- void clearStartInfoCompleteListener(final int uid, boolean unlinkDeathRecipient) {
+ void removeStartInfoCompleteListener(
+ final IApplicationStartInfoCompleteListener listener, final int uid,
+ boolean unlinkDeathRecipient) {
synchronized (mLock) {
if (!mEnabled) {
return;
}
- if (unlinkDeathRecipient) {
- ApplicationStartInfoCompleteCallback callback = mCallbacks.get(uid);
- if (callback != null) {
- callback.unlinkToDeath();
+ final ArrayList<ApplicationStartInfoCompleteCallback> callbacks = mCallbacks.get(uid);
+ if (callbacks == null) {
+ return;
+ }
+ final int size = callbacks.size();
+ int index;
+ for (index = 0; index < size; index++) {
+ final ApplicationStartInfoCompleteCallback callback = callbacks.get(index);
+ if (callback.mCallback == listener) {
+ if (unlinkDeathRecipient) {
+ callback.unlinkToDeath();
+ }
+ break;
}
}
- mCallbacks.remove(uid);
+ if (index < size) {
+ callbacks.remove(index);
+ }
+ if (callbacks.isEmpty()) {
+ mCallbacks.remove(uid);
+ }
}
}
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index 30f21a6..095d907 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -1249,9 +1249,9 @@
ProviderInfo cpi = providers.get(i);
boolean singleton = mService.isSingleton(cpi.processName, cpi.applicationInfo,
cpi.name, cpi.flags);
- if (isSingletonOrSystemUserOnly(cpi) && app.userId != UserHandle.USER_SYSTEM) {
- // This is a singleton or a SYSTEM user only provider, but a user besides the
- // SYSTEM user is asking to initialize a process it runs
+ if (singleton && app.userId != UserHandle.USER_SYSTEM) {
+ // This is a singleton provider, but a user besides the
+ // default user is asking to initialize a process it runs
// in... well, no, it doesn't actually run in this process,
// it runs in the process of the default user. Get rid of it.
providers.remove(i);
@@ -1398,7 +1398,8 @@
final boolean processMatch =
Objects.equals(pi.processName, app.processName)
|| pi.multiprocess;
- final boolean userMatch = !isSingletonOrSystemUserOnly(pi)
+ final boolean userMatch = !mService.isSingleton(
+ pi.processName, pi.applicationInfo, pi.name, pi.flags)
|| app.userId == UserHandle.USER_SYSTEM;
final boolean isInstantApp = pi.applicationInfo.isInstantApp();
final boolean splitInstalled = pi.splitName == null
@@ -1984,13 +1985,4 @@
return isAuthRedirected;
}
}
-
- /**
- * Returns true if Provider is either singleUser or systemUserOnly provider.
- */
- private boolean isSingletonOrSystemUserOnly(ProviderInfo pi) {
- return (android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
- && mService.isSystemUserOnly(pi.flags))
- || mService.isSingleton(pi.processName, pi.applicationInfo, pi.name, pi.flags);
- }
}
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index b03183c..fa5dbd2 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -2935,7 +2935,11 @@
return true;
}
- private static void freezeBinderAndPackageCgroup(ArrayList<Pair<ProcessRecord, Boolean>> procs,
+ private static boolean unfreezePackageCgroup(int packageUID) {
+ return freezePackageCgroup(packageUID, false);
+ }
+
+ private static void freezeBinderAndPackageCgroup(List<Pair<ProcessRecord, Boolean>> procs,
int packageUID) {
// Freeze all binder processes under the target UID (whose cgroup is about to be frozen).
// Since we're going to kill these, we don't need to unfreze them later.
@@ -2943,12 +2947,9 @@
// processes (forks) should not be Binder users.
int N = procs.size();
for (int i = 0; i < N; i++) {
- final int uid = procs.get(i).first.uid;
final int pid = procs.get(i).first.getPid();
int nRetries = 0;
- // We only freeze the cgroup of the target package, so we do not need to freeze the
- // Binder interfaces of dependant processes in other UIDs.
- if (pid > 0 && uid == packageUID) {
+ if (pid > 0) {
try {
int rc;
do {
@@ -2962,12 +2963,19 @@
}
// We freeze the entire UID (parent) cgroup so that newly-specialized processes also freeze
- // despite being added to a new child cgroup. The cgroups of package dependant processes are
- // not frozen, since it's possible this would freeze processes with no dependency on the
- // package being killed here.
+ // despite being added to a child cgroup created after this call that would otherwise be
+ // unfrozen.
freezePackageCgroup(packageUID, true);
}
+ private static List<Pair<ProcessRecord, Boolean>> getUIDSublist(
+ List<Pair<ProcessRecord, Boolean>> procs, int startIdx) {
+ final int uid = procs.get(startIdx).first.uid;
+ int endIdx = startIdx + 1;
+ while (endIdx < procs.size() && procs.get(endIdx).first.uid == uid) ++endIdx;
+ return procs.subList(startIdx, endIdx);
+ }
+
@GuardedBy({"mService", "mProcLock"})
boolean killPackageProcessesLSP(String packageName, int appId,
int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart,
@@ -3063,25 +3071,36 @@
}
}
- final int packageUID = UserHandle.getUid(userId, appId);
- final boolean doFreeze = appId >= Process.FIRST_APPLICATION_UID
- && appId <= Process.LAST_APPLICATION_UID;
- if (doFreeze) {
- freezeBinderAndPackageCgroup(procs, packageUID);
+ final boolean killingUserApp = appId >= Process.FIRST_APPLICATION_UID
+ && appId <= Process.LAST_APPLICATION_UID;
+
+ if (killingUserApp) {
+ procs.sort((o1, o2) -> Integer.compare(o1.first.uid, o2.first.uid));
}
- int N = procs.size();
- for (int i=0; i<N; i++) {
- final Pair<ProcessRecord, Boolean> proc = procs.get(i);
- removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second,
- reasonCode, subReason, reason, !doFreeze /* async */);
+ int idx = 0;
+ while (idx < procs.size()) {
+ final List<Pair<ProcessRecord, Boolean>> uidProcs = getUIDSublist(procs, idx);
+ final int packageUID = uidProcs.get(0).first.uid;
+
+ // Do not freeze for system apps or for dependencies of the targeted package, but
+ // make sure to freeze the targeted package for all users if called with USER_ALL.
+ final boolean doFreeze = killingUserApp && UserHandle.getAppId(packageUID) == appId;
+
+ if (doFreeze) freezeBinderAndPackageCgroup(uidProcs, packageUID);
+
+ for (Pair<ProcessRecord, Boolean> proc : uidProcs) {
+ removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second,
+ reasonCode, subReason, reason, !doFreeze /* async */);
+ }
+ killAppZygotesLocked(packageName, appId, userId, false /* force */);
+
+ if (doFreeze) unfreezePackageCgroup(packageUID);
+
+ idx += uidProcs.size();
}
- killAppZygotesLocked(packageName, appId, userId, false /* force */);
mService.updateOomAdjLocked(OOM_ADJ_REASON_PROCESS_END);
- if (doFreeze) {
- freezePackageCgroup(packageUID, false);
- }
- return N > 0;
+ return procs.size() > 0;
}
@GuardedBy("mService")
diff --git a/services/core/java/com/android/server/app/GameManagerSettings.java b/services/core/java/com/android/server/app/GameManagerSettings.java
index 5189017..b084cf3 100644
--- a/services/core/java/com/android/server/app/GameManagerSettings.java
+++ b/services/core/java/com/android/server/app/GameManagerSettings.java
@@ -251,6 +251,7 @@
+ type);
}
}
+ str.close();
} catch (XmlPullParserException | java.io.IOException e) {
Slog.wtf(TAG, "Error reading game manager settings", e);
return false;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index f80228a..99b45ec 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1812,22 +1812,21 @@
"msg: MSG_L_SET_BT_ACTIVE_DEVICE "
+ "received with null profile proxy: "
+ btInfo)).printLog(TAG));
- sendMsg(MSG_CHECK_MUTE_MUSIC, SENDMSG_REPLACE, 0 /*delay*/);
- return;
- }
- @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
- mBtHelper.getCodecWithFallback(btInfo.mDevice,
- btInfo.mProfile, btInfo.mIsLeOutput,
- "MSG_L_SET_BT_ACTIVE_DEVICE");
- mDeviceInventory.onSetBtActiveDevice(btInfo, codec,
- (btInfo.mProfile
- != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput)
- ? mAudioService.getBluetoothContextualVolumeStream()
- : AudioSystem.STREAM_DEFAULT);
- if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
- || btInfo.mProfile == BluetoothProfile.HEARING_AID) {
- onUpdateCommunicationRouteClient(isBluetoothScoRequested(),
- "setBluetoothActiveDevice");
+ } else {
+ @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
+ mBtHelper.getCodecWithFallback(btInfo.mDevice,
+ btInfo.mProfile, btInfo.mIsLeOutput,
+ "MSG_L_SET_BT_ACTIVE_DEVICE");
+ mDeviceInventory.onSetBtActiveDevice(btInfo, codec,
+ (btInfo.mProfile
+ != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput)
+ ? mAudioService.getBluetoothContextualVolumeStream()
+ : AudioSystem.STREAM_DEFAULT);
+ if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
+ || btInfo.mProfile == BluetoothProfile.HEARING_AID) {
+ onUpdateCommunicationRouteClient(isBluetoothScoRequested(),
+ "setBluetoothActiveDevice");
+ }
}
}
}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index bf20ae3..57b19cd 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -764,7 +764,7 @@
/** only public for mocking/spying, do not call outside of AudioService */
// @GuardedBy("mDeviceBroker.mSetModeLock")
@VisibleForTesting
- @GuardedBy("mDeviceBroker.mDeviceStateLock")
+ //@GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
public void onSetBtActiveDevice(@NonNull AudioDeviceBroker.BtDeviceInfo btInfo,
@AudioSystem.AudioFormatNativeEnumForBtCodec int codec,
int streamType) {
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index 823788f..b179783 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -137,9 +137,9 @@
public abstract boolean isAppRunningOnAnyVirtualDevice(int uid);
/**
- * Returns true if the {@code displayId} is owned by any virtual device
+ * @return whether the input device with the given id was created by a virtual device.
*/
- public abstract boolean isDisplayOwnedByAnyVirtualDevice(int displayId);
+ public abstract boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId);
/**
* Gets the ids of VirtualDisplays owned by a VirtualDevice.
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 8910b6e..082776a 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -624,10 +624,10 @@
pw.println(" Current mode="
+ autoBrightnessModeToString(mCurrentBrightnessMapper.getMode()));
- pw.println();
for (int i = 0; i < mBrightnessMappingStrategyMap.size(); i++) {
+ pw.println();
pw.println(" Mapper for mode "
- + autoBrightnessModeToString(mBrightnessMappingStrategyMap.keyAt(i)) + "=");
+ + autoBrightnessModeToString(mBrightnessMappingStrategyMap.keyAt(i)) + ":");
mBrightnessMappingStrategyMap.valueAt(i).dump(pw,
mBrightnessRangeController.getNormalBrightnessMax());
}
@@ -1159,7 +1159,7 @@
if (mCurrentBrightnessMapper.getMode() == mode) {
return;
}
- Slog.i(TAG, "Switching to mode " + mode);
+ Slog.i(TAG, "Switching to mode " + autoBrightnessModeToString(mode));
if (mode == AUTO_BRIGHTNESS_MODE_IDLE
|| mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_IDLE) {
switchModeAndShortTermModels(mode);
diff --git a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
index 544f490..e0bdda5 100644
--- a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
+++ b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
@@ -165,8 +165,15 @@
*/
public float[] getLuxArray(@AutomaticBrightnessController.AutomaticBrightnessMode int mode,
int preset) {
- return mBrightnessLevelsLuxMap.get(
+ float[] luxArray = mBrightnessLevelsLuxMap.get(
autoBrightnessModeToString(mode) + "_" + autoBrightnessPresetToString(preset));
+ if (luxArray != null) {
+ return luxArray;
+ }
+
+ // No array for this preset, fall back to the normal preset
+ return mBrightnessLevelsLuxMap.get(autoBrightnessModeToString(mode) + "_"
+ + AutoBrightnessSettingName.normal.getRawName());
}
/**
@@ -184,8 +191,15 @@
*/
public float[] getBrightnessArray(
@AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset) {
- return mBrightnessLevelsMap.get(
+ float[] brightnessArray = mBrightnessLevelsMap.get(
autoBrightnessModeToString(mode) + "_" + autoBrightnessPresetToString(preset));
+ if (brightnessArray != null) {
+ return brightnessArray;
+ }
+
+ // No array for this preset, fall back to the normal preset
+ return mBrightnessLevelsMap.get(autoBrightnessModeToString(mode) + "_"
+ + AutoBrightnessSettingName.normal.getRawName());
}
@Override
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index be48eb4..1ae2559 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -100,6 +100,10 @@
Flags.FLAG_ENABLE_VSYNC_LOW_POWER_VOTE,
Flags::enableVsyncLowPowerVote);
+ private final FlagState mVsyncLowLightVote = new FlagState(
+ Flags.FLAG_ENABLE_VSYNC_LOW_LIGHT_VOTE,
+ Flags::enableVsyncLowLightVote);
+
private final FlagState mBrightnessWearBedtimeModeClamperFlagState = new FlagState(
Flags.FLAG_BRIGHTNESS_WEAR_BEDTIME_MODE_CLAMPER,
Flags::brightnessWearBedtimeModeClamper);
@@ -220,6 +224,10 @@
return mVsyncLowPowerVote.isEnabled();
}
+ public boolean isVsyncLowLightVoteEnabled() {
+ return mVsyncLowLightVote.isEnabled();
+ }
+
public boolean isBrightnessWearBedtimeModeClamperEnabled() {
return mBrightnessWearBedtimeModeClamperFlagState.isEnabled();
}
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index a2319a8..c2f52b5 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -146,6 +146,14 @@
}
flag {
+ name: "enable_vsync_low_light_vote"
+ namespace: "display_manager"
+ description: "Feature flag for vsync low light vote"
+ bug: "314921657"
+ is_fixed_read_only: true
+}
+
+flag {
name: "brightness_wear_bedtime_mode_clamper"
namespace: "display_manager"
description: "Feature flag for the Wear Bedtime mode brightness clamper"
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index ad3deff..8707000 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -215,7 +215,8 @@
mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings();
mSettingsObserver = new SettingsObserver(context, handler, mDvrrSupported,
displayManagerFlags);
- mBrightnessObserver = new BrightnessObserver(context, handler, injector);
+ mBrightnessObserver = new BrightnessObserver(context, handler, injector, mDvrrSupported,
+ displayManagerFlags);
mDefaultDisplayDeviceConfig = null;
mUdfpsObserver = new UdfpsObserver();
mVotesStorage = new VotesStorage(this::notifyDesiredDisplayModeSpecsChangedLocked,
@@ -1510,6 +1511,8 @@
private final Injector mInjector;
private final Handler mHandler;
+ private final boolean mVsyncLowLightBlockingVoteEnabled;
+
private final IThermalEventListener.Stub mThermalListener =
new IThermalEventListener.Stub() {
@Override
@@ -1544,7 +1547,8 @@
@GuardedBy("mLock")
private @Temperature.ThrottlingStatus int mThermalStatus = Temperature.THROTTLING_NONE;
- BrightnessObserver(Context context, Handler handler, Injector injector) {
+ BrightnessObserver(Context context, Handler handler, Injector injector,
+ boolean dvrrSupported , DisplayManagerFlags flags) {
mContext = context;
mHandler = handler;
mInjector = injector;
@@ -1552,6 +1556,7 @@
/* attemptReadFromFeatureParams= */ false);
mRefreshRateInHighZone = context.getResources().getInteger(
R.integer.config_fixedRefreshRateInHighZone);
+ mVsyncLowLightBlockingVoteEnabled = dvrrSupported && flags.isVsyncLowLightVoteEnabled();
}
/**
@@ -2131,7 +2136,17 @@
Vote.forPhysicalRefreshRates(range.min, range.max);
}
}
- refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
+
+ if (mVsyncLowLightBlockingVoteEnabled) {
+ refreshRateSwitchingVote = Vote.forSupportedModesAndDisableRefreshRateSwitching(
+ List.of(
+ new SupportedModesVote.SupportedMode(
+ /* peakRefreshRate= */ 60f, /* vsyncRate= */ 60f),
+ new SupportedModesVote.SupportedMode(
+ /* peakRefreshRate= */120f, /* vsyncRate= */ 120f)));
+ } else {
+ refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
+ }
}
boolean insideHighZone = hasValidHighZone()
diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java
index 8f39570..e8d5a19 100644
--- a/services/core/java/com/android/server/display/mode/Vote.java
+++ b/services/core/java/com/android/server/display/mode/Vote.java
@@ -170,6 +170,13 @@
return new SupportedModesVote(supportedModes);
}
+
+ static Vote forSupportedModesAndDisableRefreshRateSwitching(
+ List<SupportedModesVote.SupportedMode> supportedModes) {
+ return new CombinedVote(
+ List.of(forDisableRefreshRateSwitching(), forSupportedModes(supportedModes)));
+ }
+
static String priorityToString(int priority) {
switch (priority) {
case PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE:
diff --git a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
index a30c4d2..e80b9451 100644
--- a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
+++ b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
@@ -16,12 +16,19 @@
package com.android.server.display.mode;
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED;
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ACTIVE;
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED;
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Trace;
import android.util.SparseArray;
import android.view.Display;
+import com.android.internal.util.FrameworkStatsLog;
+
/**
* The VotesStatsReporter is responsible for collecting and sending Vote related statistics
*/
@@ -31,42 +38,77 @@
private final boolean mIgnoredRenderRate;
private final boolean mFrameworkStatsLogReportingEnabled;
+ private int mLastMinPriorityReported = Vote.MAX_PRIORITY + 1;
+
public VotesStatsReporter(boolean ignoreRenderRate, boolean refreshRateVotingTelemetryEnabled) {
mIgnoredRenderRate = ignoreRenderRate;
mFrameworkStatsLogReportingEnabled = refreshRateVotingTelemetryEnabled;
}
- void reportVoteAdded(int displayId, int priority, @NonNull Vote vote) {
+ void reportVoteChanged(int displayId, int priority, @Nullable Vote vote) {
+ if (vote == null) {
+ reportVoteRemoved(displayId, priority);
+ } else {
+ reportVoteAdded(displayId, priority, vote);
+ }
+ }
+
+ private void reportVoteAdded(int displayId, int priority, @NonNull Vote vote) {
int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
Trace.traceCounter(Trace.TRACE_TAG_POWER,
TAG + "." + displayId + ":" + Vote.priorityToString(priority), maxRefreshRate);
- // if ( mFrameworkStatsLogReportingEnabled) {
- // FrameworkStatsLog.write(VOTE_CHANGED, displayID, priority, ADDED, maxRefreshRate, -1);
- // }
+ if (mFrameworkStatsLogReportingEnabled) {
+ FrameworkStatsLog.write(
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED,
+ maxRefreshRate, -1);
+ }
}
- void reportVoteRemoved(int displayId, int priority) {
+ private void reportVoteRemoved(int displayId, int priority) {
Trace.traceCounter(Trace.TRACE_TAG_POWER,
TAG + "." + displayId + ":" + Vote.priorityToString(priority), -1);
- // if ( mFrameworkStatsLogReportingEnabled) {
- // FrameworkStatsLog.write(VOTE_CHANGED, displayID, priority, REMOVED, -1, -1);
- // }
+ if (mFrameworkStatsLogReportingEnabled) {
+ FrameworkStatsLog.write(
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1);
+ }
}
void reportVotesActivated(int displayId, int minPriority, @Nullable Display.Mode baseMode,
SparseArray<Vote> votes) {
-// if (!mFrameworkStatsLogReportingEnabled) {
-// return;
-// }
-// int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1;
-// for (int priority = minPriority; priority <= Vote.MAX_PRIORITY; priority ++) {
-// Vote vote = votes.get(priority);
-// if (vote != null) {
-// int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
-// FrameworkStatsLog.write(VOTE_CHANGED, displayId, priority,
-// ACTIVE, maxRefreshRate, selectedRefreshRate);
-// }
-// }
+ if (!mFrameworkStatsLogReportingEnabled) {
+ return;
+ }
+ int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1;
+ for (int priority = Vote.MIN_PRIORITY; priority <= Vote.MAX_PRIORITY; priority++) {
+ if (priority < mLastMinPriorityReported && priority < minPriority) {
+ continue;
+ }
+ Vote vote = votes.get(priority);
+ if (vote == null) {
+ continue;
+ }
+
+ // Was previously reported ACTIVE, changed to ADDED
+ if (priority >= mLastMinPriorityReported && priority < minPriority) {
+ int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
+ FrameworkStatsLog.write(
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED,
+ maxRefreshRate, selectedRefreshRate);
+ }
+ // Was previously reported ADDED, changed to ACTIVE
+ if (priority >= minPriority && priority < mLastMinPriorityReported) {
+ int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
+ FrameworkStatsLog.write(
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ACTIVE,
+ maxRefreshRate, selectedRefreshRate);
+ }
+
+ mLastMinPriorityReported = minPriority;
+ }
}
private static int getMaxRefreshRate(@NonNull Vote vote, boolean ignoreRenderRate) {
diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java
index 7a1f7e9..56c7c18 100644
--- a/services/core/java/com/android/server/display/mode/VotesStorage.java
+++ b/services/core/java/com/android/server/display/mode/VotesStorage.java
@@ -117,22 +117,13 @@
Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes);
}
if (changed) {
- reportVoteStats(displayId, priority, vote);
+ if (mVotesStatsReporter != null) {
+ mVotesStatsReporter.reportVoteChanged(displayId, priority, vote);
+ }
mListener.onChanged();
}
}
- private void reportVoteStats(int displayId, int priority, @Nullable Vote vote) {
- if (mVotesStatsReporter == null) {
- return;
- }
- if (vote == null) {
- mVotesStatsReporter.reportVoteRemoved(displayId, priority);
- } else {
- mVotesStatsReporter.reportVoteAdded(displayId, priority, vote);
- }
- }
-
/** dump class values, for debugging */
void dump(@NonNull PrintWriter pw) {
SparseArray<SparseArray<Vote>> votesByDisplayLocal = new SparseArray<>();
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 1cd267d..d34661d 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1290,15 +1290,19 @@
mService.getHdmiCecNetwork().removeCecSwitches(portId);
}
- // Turning System Audio Mode off when the AVR is unlugged or standby.
- // When the device is not unplugged but reawaken from standby, we check if the System
- // Audio Control Feature is enabled or not then decide if turning SAM on/off accordingly.
- if (getAvrDeviceInfo() != null && portId == getAvrDeviceInfo().getPortId()) {
- HdmiLogger.debug("Port ID:%d, 5v=%b", portId, connected);
- if (!connected) {
- setSystemAudioMode(false);
- } else {
- onNewAvrAdded(getAvrDeviceInfo());
+ if (!mService.isEarcEnabled() || !mService.isEarcSupported()) {
+ HdmiDeviceInfo avr = getAvrDeviceInfo();
+ if (avr != null
+ && portId == avr.getPortId()
+ && isConnectedToArcPort(avr.getPhysicalAddress())) {
+ HdmiLogger.debug("Port ID:%d, 5v=%b", portId, connected);
+ if (connected) {
+ if (mArcEstablished) {
+ enableAudioReturnChannel(true);
+ }
+ } else {
+ enableAudioReturnChannel(false);
+ }
}
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index eaf754d..e0e825d 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -3617,7 +3617,7 @@
}
}
- @VisibleForTesting
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
protected boolean isEarcSupported() {
synchronized (mLock) {
return mEarcSupported;
diff --git a/services/core/java/com/android/server/hdmi/NewDeviceAction.java b/services/core/java/com/android/server/hdmi/NewDeviceAction.java
index f3532e5..b6c0e5d 100644
--- a/services/core/java/com/android/server/hdmi/NewDeviceAction.java
+++ b/services/core/java/com/android/server/hdmi/NewDeviceAction.java
@@ -53,6 +53,7 @@
private int mVendorId;
private String mDisplayName;
private int mTimeoutRetry;
+ private HdmiDeviceInfo mOldDeviceInfo;
/**
* Constructor.
@@ -73,6 +74,38 @@
@Override
public boolean start() {
+ mOldDeviceInfo =
+ localDevice().mService.getHdmiCecNetwork().getCecDeviceInfo(mDeviceLogicalAddress);
+ // If there's deviceInfo with same (logical address, physical address) set
+ // Then addCecDevice should be delayed until system information process is finished
+ if (mOldDeviceInfo != null
+ && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress) {
+ Slog.d(TAG, "Start NewDeviceAction with old deviceInfo:["
+ + mOldDeviceInfo.toString() + "]");
+ } else {
+ // Add the device ahead with default information to handle <Active Source>
+ // promptly, rather than waiting till the new device action is finished.
+ Slog.d(TAG, "Start NewDeviceAction with default deviceInfo");
+ HdmiDeviceInfo deviceInfo = HdmiDeviceInfo.cecDeviceBuilder()
+ .setLogicalAddress(mDeviceLogicalAddress)
+ .setPhysicalAddress(mDevicePhysicalAddress)
+ .setPortId(tv().getPortId(mDevicePhysicalAddress))
+ .setDeviceType(mDeviceType)
+ .setVendorId(Constants.VENDOR_ID_UNKNOWN)
+ .build();
+ // If a deviceInfo with same logical address but different physical address exists
+ // We should remove the old deviceInfo first
+ // This will happen if the interval between unplugging and plugging device is too short
+ // and HotplugDetection Action fails to remove the old deviceInfo, or when the newly
+ // plugged device violates HDMI Spec and uses an occupied logical address
+ if (mOldDeviceInfo != null) {
+ Slog.d(TAG, "Remove device by NewDeviceAction, logical address conflicts: "
+ + mDevicePhysicalAddress);
+ localDevice().mService.getHdmiCecNetwork().removeCecDevice(
+ localDevice(), mDeviceLogicalAddress);
+ }
+ localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo);
+ }
requestOsdName(true);
return true;
}
@@ -182,14 +215,30 @@
.setVendorId(mVendorId)
.setDisplayName(mDisplayName)
.build();
- localDevice().mService.getHdmiCecNetwork().updateCecDevice(deviceInfo);
- // Consume CEC messages we already got for this newly found device.
- tv().processDelayedMessages(mDeviceLogicalAddress);
+ // Check if oldDevice is same as newDevice
+ // If so, don't add newDevice info, preventing ARC or HDMI source re-connection
+ if (mOldDeviceInfo != null
+ && mOldDeviceInfo.getLogicalAddress() == mDeviceLogicalAddress
+ && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress
+ && mOldDeviceInfo.getDeviceType() == mDeviceType
+ && mOldDeviceInfo.getVendorId() == mVendorId
+ && mOldDeviceInfo.getDisplayName().equals(mDisplayName)) {
+ // Consume CEC messages we already got for this newly found device.
+ tv().processDelayedMessages(mDeviceLogicalAddress);
+ Slog.d(TAG, "Ignore NewDevice, deviceInfo is same as current device");
+ Slog.d(TAG, "Old:[" + mOldDeviceInfo.toString()
+ + "]; New:[" + deviceInfo.toString() + "]");
+ } else {
+ Slog.d(TAG, "Add NewDevice:[" + deviceInfo.toString() + "]");
+ localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo);
- if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM,
- mDeviceLogicalAddress)) {
- tv().onNewAvrAdded(deviceInfo);
+ // Consume CEC messages we already got for this newly found device.
+ tv().processDelayedMessages(mDeviceLogicalAddress);
+ if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM,
+ mDeviceLogicalAddress)) {
+ tv().onNewAvrAdded(deviceInfo);
+ }
}
}
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 8580b96..46668de 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -76,6 +76,8 @@
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.XmlUtils;
+import com.android.server.LocalServices;
+import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent;
import com.android.server.input.KeyboardMetricsCollector.LayoutSelectionCriteria;
import com.android.server.inputmethod.InputMethodManagerInternal;
@@ -197,7 +199,7 @@
final KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice);
KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId);
if (config == null) {
- config = new KeyboardConfiguration();
+ config = new KeyboardConfiguration(deviceId);
mConfiguredKeyboards.put(deviceId, config);
}
@@ -1093,19 +1095,26 @@
@MainThread
private void maybeUpdateNotification() {
- if (mConfiguredKeyboards.size() == 0) {
- hideKeyboardLayoutNotification();
- return;
- }
+ List<KeyboardConfiguration> configurations = new ArrayList<>();
for (int i = 0; i < mConfiguredKeyboards.size(); i++) {
+ int deviceId = mConfiguredKeyboards.keyAt(i);
+ KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i);
+ if (isVirtualDevice(deviceId)) {
+ continue;
+ }
// If we have a keyboard with no selected layouts, we should always show missing
// layout notification even if there are other keyboards that are configured properly.
- if (!mConfiguredKeyboards.valueAt(i).hasConfiguredLayouts()) {
+ if (!config.hasConfiguredLayouts()) {
showMissingKeyboardLayoutNotification();
return;
}
+ configurations.add(config);
}
- showConfiguredKeyboardLayoutNotification();
+ if (configurations.size() == 0) {
+ hideKeyboardLayoutNotification();
+ return;
+ }
+ showConfiguredKeyboardLayoutNotification(configurations);
}
@MainThread
@@ -1185,10 +1194,11 @@
}
@MainThread
- private void showConfiguredKeyboardLayoutNotification() {
+ private void showConfiguredKeyboardLayoutNotification(
+ List<KeyboardConfiguration> configurations) {
final Resources r = mContext.getResources();
- if (mConfiguredKeyboards.size() != 1) {
+ if (configurations.size() != 1) {
showKeyboardLayoutNotification(
r.getString(R.string.keyboard_layout_notification_multiple_selected_title),
r.getString(R.string.keyboard_layout_notification_multiple_selected_message),
@@ -1196,8 +1206,8 @@
return;
}
- final InputDevice inputDevice = getInputDevice(mConfiguredKeyboards.keyAt(0));
- final KeyboardConfiguration config = mConfiguredKeyboards.valueAt(0);
+ final KeyboardConfiguration config = configurations.get(0);
+ final InputDevice inputDevice = getInputDevice(config.getDeviceId());
if (inputDevice == null || !config.hasConfiguredLayouts()) {
return;
}
@@ -1356,6 +1366,13 @@
return false;
}
+ @VisibleForTesting
+ public boolean isVirtualDevice(int deviceId) {
+ VirtualDeviceManagerInternal vdm = LocalServices.getService(
+ VirtualDeviceManagerInternal.class);
+ return vdm != null && vdm.isInputDeviceOwnedByVirtualDevice(deviceId);
+ }
+
private static int[] getScriptCodes(@Nullable Locale locale) {
if (locale == null) {
return new int[0];
@@ -1430,11 +1447,22 @@
}
private static class KeyboardConfiguration {
+
// If null or empty, it means no layout is configured for the device. And user needs to
// manually set up the device.
@Nullable
private Set<String> mConfiguredLayouts;
+ private final int mDeviceId;
+
+ private KeyboardConfiguration(int deviceId) {
+ mDeviceId = deviceId;
+ }
+
+ private int getDeviceId() {
+ return mDeviceId;
+ }
+
private boolean hasConfiguredLayouts() {
return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty();
}
diff --git a/services/core/java/com/android/server/inputmethod/ClientController.java b/services/core/java/com/android/server/inputmethod/ClientController.java
index 2934640..21b952b 100644
--- a/services/core/java/com/android/server/inputmethod/ClientController.java
+++ b/services/core/java/com/android/server/inputmethod/ClientController.java
@@ -25,9 +25,13 @@
import android.view.inputmethod.InputBinding;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.IInputMethodClient;
import com.android.internal.inputmethod.IRemoteInputConnection;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Store and manage {@link InputMethodManagerService} clients. This class was designed to be a
* singleton in {@link InputMethodManagerService} since it stores information about all clients,
@@ -37,9 +41,7 @@
* As part of the re-architecture plan (described in go/imms-rearchitecture-plan), the following
* fields and methods will be moved out from IMMS and placed here:
* <ul>
- * <li>mCurClient (ClientState)</li>
* <li>mClients (ArrayMap of ClientState indexed by IBinder)</li>
- * <li>mLastSwitchUserId</li>
* </ul>
* <p>
* Nested Classes (to move from IMMS):
@@ -54,7 +56,6 @@
* <li>removeClient</li>
* <li>verifyClientAndPackageMatch</li>
* <li>setImeTraceEnabledForAllClients (make it reactive)</li>
- * <li>unbindCurrentClient</li>
* </ul>
*/
// TODO(b/314150112): Update the Javadoc above, by removing the re-architecture steps, once this
@@ -65,18 +66,32 @@
@GuardedBy("ImfLock.class")
final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>();
+ @GuardedBy("ImfLock.class")
+ private final List<ClientControllerCallback> mCallbacks = new ArrayList<>();
+
private final PackageManagerInternal mPackageManagerInternal;
+ interface ClientControllerCallback {
+
+ void onClientRemoved(ClientState client);
+ }
+
ClientController(PackageManagerInternal packageManagerInternal) {
mPackageManagerInternal = packageManagerInternal;
}
@GuardedBy("ImfLock.class")
- void addClient(IInputMethodClientInvoker clientInvoker,
- IRemoteInputConnection inputConnection,
- int selfReportedDisplayId, IBinder.DeathRecipient deathRecipient, int callerUid,
+ ClientState addClient(IInputMethodClientInvoker clientInvoker,
+ IRemoteInputConnection inputConnection, int selfReportedDisplayId, int callerUid,
int callerPid) {
- // TODO: Optimize this linear search.
+ final IBinder.DeathRecipient deathRecipient = () -> {
+ // Exceptionally holding ImfLock here since this is a internal lambda expression.
+ synchronized (ImfLock.class) {
+ removeClientAsBinder(clientInvoker.asBinder());
+ }
+ };
+
+ // TODO(b/319457906): Optimize this linear search.
final int numClients = mClients.size();
for (int i = 0; i < numClients; ++i) {
final ClientState state = mClients.valueAt(i);
@@ -101,14 +116,40 @@
// have the client crash. Thus we do not verify the display ID at all here. Instead we
// later check the display ID every time the client needs to interact with the specified
// display.
- mClients.put(clientInvoker.asBinder(), new ClientState(clientInvoker, inputConnection,
- callerUid, callerPid, selfReportedDisplayId, deathRecipient));
+ final ClientState cs = new ClientState(clientInvoker, inputConnection,
+ callerUid, callerPid, selfReportedDisplayId, deathRecipient);
+ mClients.put(clientInvoker.asBinder(), cs);
+ return cs;
+ }
+
+ @VisibleForTesting
+ @GuardedBy("ImfLock.class")
+ boolean removeClient(IInputMethodClient client) {
+ return removeClientAsBinder(client.asBinder());
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean removeClientAsBinder(IBinder binder) {
+ final ClientState cs = mClients.remove(binder);
+ if (cs == null) {
+ return false;
+ }
+ binder.unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */);
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).onClientRemoved(cs);
+ }
+ return true;
+ }
+
+ @GuardedBy("ImfLock.class")
+ void addClientControllerCallback(ClientControllerCallback callback) {
+ mCallbacks.add(callback);
}
@GuardedBy("ImfLock.class")
boolean verifyClientAndPackageMatch(
@NonNull IInputMethodClient client, @NonNull String packageName) {
- ClientState cs = mClients.get(client.asBinder());
+ final ClientState cs = mClients.get(client.asBinder());
if (cs == null) {
throw new IllegalArgumentException("unknown client " + client.asBinder());
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 622a2de..8448fc2 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -478,7 +478,6 @@
/**
* The client that is currently bound to an input method.
*/
- // TODO(b/314150112): Move this to ClientController.
@Nullable
private ClientState mCurClient;
@@ -1676,7 +1675,11 @@
mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
mVisibilityApplier = new DefaultImeVisibilityApplier(this);
+
mClientController = new ClientController(mPackageManagerInternal);
+ synchronized (ImfLock.class) {
+ mClientController.addClientControllerCallback(c -> onClientRemoved(c));
+ }
mPreventImeStartupUnlessTextEditor = mRes.getBoolean(
com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
@@ -2168,47 +2171,41 @@
// actually running.
final int callerUid = Binder.getCallingUid();
final int callerPid = Binder.getCallingPid();
-
- // TODO(b/314150112): Move the death recipient logic to ClientController when moving
- // removeClient method.
- final IBinder.DeathRecipient deathRecipient = () -> removeClient(client);
final IInputMethodClientInvoker clientInvoker =
IInputMethodClientInvoker.create(client, mHandler);
synchronized (ImfLock.class) {
mClientController.addClient(clientInvoker, inputConnection, selfReportedDisplayId,
- deathRecipient, callerUid, callerPid);
+ callerUid, callerPid);
}
}
- // TODO(b/314150112): Move this to ClientController.
- void removeClient(IInputMethodClient client) {
+ // TODO(b/314150112): Move this method to InputMethodBindingController
+ /**
+ * Hide the IME if the removed user is the current user.
+ */
+ private void onClientRemoved(ClientController.ClientState client) {
synchronized (ImfLock.class) {
- ClientState cs = mClientController.mClients.remove(client.asBinder());
- if (cs != null) {
- client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */);
- clearClientSessionLocked(cs);
- clearClientSessionForAccessibilityLocked(cs);
-
- if (mCurClient == cs) {
- hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
- null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
- if (mBoundToMethod) {
- mBoundToMethod = false;
- IInputMethodInvoker curMethod = getCurMethodLocked();
- if (curMethod != null) {
- // When we unbind input, we are unbinding the client, so we always
- // unbind ime and a11y together.
- curMethod.unbindInput();
- AccessibilityManagerInternal.get().unbindInput();
- }
+ clearClientSessionLocked(client);
+ clearClientSessionForAccessibilityLocked(client);
+ if (mCurClient == client) {
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
+ if (mBoundToMethod) {
+ mBoundToMethod = false;
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null) {
+ // When we unbind input, we are unbinding the client, so we always
+ // unbind ime and a11y together.
+ curMethod.unbindInput();
+ AccessibilityManagerInternal.get().unbindInput();
}
- mBoundToAccessibility = false;
- mCurClient = null;
}
- if (mCurFocusedWindowClient == cs) {
- mCurFocusedWindowClient = null;
- mCurFocusedWindowEditorInfo = null;
- }
+ mBoundToAccessibility = false;
+ mCurClient = null;
+ }
+ if (mCurFocusedWindowClient == client) {
+ mCurFocusedWindowClient = null;
+ mCurFocusedWindowEditorInfo = null;
}
}
}
@@ -2218,8 +2215,7 @@
void unbindCurrentClientLocked(@UnbindReason int unbindClientReason) {
if (mCurClient != null) {
if (DEBUG) {
- Slog.v(TAG, "unbindCurrentInputLocked: client="
- + mCurClient.mClient.asBinder());
+ Slog.v(TAG, "unbindCurrentInputLocked: client=" + mCurClient.mClient.asBinder());
}
if (mBoundToMethod) {
mBoundToMethod = false;
@@ -2312,7 +2308,8 @@
final StartInputInfo info = new StartInputInfo(mSettings.getCurrentUserId(),
getCurTokenLocked(),
mCurTokenDisplayId, getCurIdLocked(), startInputReason, restarting,
- UserHandle.getUserId(mCurClient.mUid), mCurClient.mSelfReportedDisplayId,
+ UserHandle.getUserId(mCurClient.mUid),
+ mCurClient.mSelfReportedDisplayId,
mCurFocusedWindow, mCurEditorInfo, mCurFocusedWindowSoftInputMode,
getSequenceNumberLocked());
mImeTargetWindowMap.put(startInputToken, mCurFocusedWindow);
@@ -2323,14 +2320,14 @@
// same-user scenarios.
// That said ignoring cross-user scenario will never affect IMEs that do not have
// INTERACT_ACROSS_USERS(_FULL) permissions, which is actually almost always the case.
- if (mSettings.getCurrentUserId() == UserHandle.getUserId(mCurClient.mUid)) {
+ if (mSettings.getCurrentUserId() == UserHandle.getUserId(
+ mCurClient.mUid)) {
mPackageManagerInternal.grantImplicitAccess(mSettings.getCurrentUserId(),
null /* intent */, UserHandle.getAppId(getCurMethodUidLocked()),
mCurClient.mUid, true /* direct */);
}
- @InputMethodNavButtonFlags
- final int navButtonFlags = getInputMethodNavButtonFlagsLocked();
+ @InputMethodNavButtonFlags final int navButtonFlags = getInputMethodNavButtonFlagsLocked();
final SessionState session = mCurClient.mCurSession;
setEnabledSessionLocked(session);
session.mMethod.startInput(startInputToken, mCurInputConnection, mCurEditorInfo, restarting,
@@ -2750,8 +2747,8 @@
&& curMethod.asBinder() == method.asBinder()) {
if (mCurClient != null) {
clearClientSessionLocked(mCurClient);
- mCurClient.mCurSession = new SessionState(mCurClient,
- method, session, channel);
+ mCurClient.mCurSession = new SessionState(
+ mCurClient, method, session, channel);
InputBindResult res = attachNewInputLocked(
StartInputReason.SESSION_CREATED_BY_IME, true);
attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_IME, true);
@@ -5776,8 +5773,10 @@
// TODO(b/305829876): Implement user ID verification
if (mCurClient != null) {
clearClientSessionForAccessibilityLocked(mCurClient, accessibilityConnectionId);
- mCurClient.mAccessibilitySessions.put(accessibilityConnectionId,
- new AccessibilitySessionState(mCurClient, accessibilityConnectionId,
+ mCurClient.mAccessibilitySessions.put(
+ accessibilityConnectionId,
+ new AccessibilitySessionState(mCurClient,
+ accessibilityConnectionId,
session));
attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_ACCESSIBILITY,
@@ -5811,7 +5810,8 @@
}
// A11yManagerService unbinds the disabled accessibility service. We don't need
// to do it here.
- mCurClient.mClient.onUnbindAccessibilityService(getSequenceNumberLocked(),
+ mCurClient.mClient.onUnbindAccessibilityService(
+ getSequenceNumberLocked(),
accessibilityConnectionId);
}
// We only have sessions when we bound to an input method. Remove this session
diff --git a/services/core/java/com/android/server/media/MediaFeatureFlagManager.java b/services/core/java/com/android/server/media/MediaFeatureFlagManager.java
deleted file mode 100644
index f90f64a..0000000
--- a/services/core/java/com/android/server/media/MediaFeatureFlagManager.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2023 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.media;
-
-import android.annotation.StringDef;
-import android.app.ActivityThread;
-import android.app.Application;
-import android.provider.DeviceConfig;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/* package */ class MediaFeatureFlagManager {
-
- /**
- * Namespace for media better together features.
- */
- private static final String NAMESPACE_MEDIA_BETTER_TOGETHER = "media_better_together";
-
- @StringDef(
- prefix = "FEATURE_",
- value = {
- FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE
- })
- @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
- @Retention(RetentionPolicy.SOURCE)
- /* package */ @interface MediaFeatureFlag {}
-
- /**
- * Whether to use IMPORTANCE_FOREGROUND (i.e. 100) or IMPORTANCE_FOREGROUND_SERVICE (i.e. 125)
- * as the minimum package importance for scanning.
- */
- /* package */ static final @MediaFeatureFlag String
- FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE = "scanning_package_minimum_importance";
-
- private static final MediaFeatureFlagManager sInstance = new MediaFeatureFlagManager();
-
- private MediaFeatureFlagManager() {
- // Empty to prevent instantiation.
- }
-
- /* package */ static MediaFeatureFlagManager getInstance() {
- return sInstance;
- }
-
- /**
- * Returns a boolean value from {@link DeviceConfig} from the system_time namespace, or
- * {@code defaultValue} if there is no explicit value set.
- */
- public boolean getBoolean(@MediaFeatureFlag String key, boolean defaultValue) {
- return DeviceConfig.getBoolean(NAMESPACE_MEDIA_BETTER_TOGETHER, key, defaultValue);
- }
-
- /**
- * Returns an int value from {@link DeviceConfig} from the system_time namespace, or {@code
- * defaultValue} if there is no explicit value set.
- */
- public int getInt(@MediaFeatureFlag String key, int defaultValue) {
- return DeviceConfig.getInt(NAMESPACE_MEDIA_BETTER_TOGETHER, key, defaultValue);
- }
-
- /**
- * Adds a listener to react for changes in media feature flags values. Future calls to this
- * method with the same listener will replace the old namespace and executor.
- *
- * @param onPropertiesChangedListener The listener to add.
- */
- public void addOnPropertiesChangedListener(
- DeviceConfig.OnPropertiesChangedListener onPropertiesChangedListener) {
- Application currentApplication = ActivityThread.currentApplication();
- if (currentApplication != null) {
- DeviceConfig.addOnPropertiesChangedListener(
- NAMESPACE_MEDIA_BETTER_TOGETHER,
- currentApplication.getMainExecutor(),
- onPropertiesChangedListener);
- }
- }
-}
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index e048522..28a1c7a 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -16,7 +16,7 @@
package com.android.server.media;
-import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.content.Intent.ACTION_SCREEN_OFF;
import static android.content.Intent.ACTION_SCREEN_ON;
import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
@@ -24,7 +24,6 @@
import static android.media.MediaRouter2Utils.getProviderId;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-import static com.android.server.media.MediaFeatureFlagManager.FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE;
import android.Manifest;
import android.annotation.NonNull;
@@ -55,7 +54,6 @@
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.UserHandle;
-import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
@@ -97,11 +95,7 @@
// in MediaRouter2, remove this constant and replace the usages with the real request IDs.
private static final long DUMMY_REQUEST_ID = -1;
- private static int sPackageImportanceForScanning =
- MediaFeatureFlagManager.getInstance()
- .getInt(
- FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE,
- IMPORTANCE_FOREGROUND_SERVICE);
+ private static final int REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING = IMPORTANCE_FOREGROUND;
/**
* Contains the list of bluetooth permissions that are required to do system routing.
@@ -159,7 +153,7 @@
mContext = context;
mActivityManager = mContext.getSystemService(ActivityManager.class);
mActivityManager.addOnUidImportanceListener(mOnUidImportanceListener,
- sPackageImportanceForScanning);
+ REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING);
mPowerManager = mContext.getSystemService(PowerManager.class);
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
@@ -171,9 +165,6 @@
}
mContext.getPackageManager().addOnPermissionsChangeListener(this::onPermissionsChanged);
-
- MediaFeatureFlagManager.getInstance()
- .addOnPropertiesChangedListener(this::onDeviceConfigChange);
}
/**
@@ -1443,8 +1434,13 @@
return;
}
- Slog.i(TAG, TextUtils.formatSimple(
- "startScan | manager: %d", managerRecord.mManagerId));
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "startScan | manager: %d, ownerPackageName: %s, targetPackageName: %s",
+ managerRecord.mManagerId,
+ managerRecord.mOwnerPackageName,
+ managerRecord.mTargetPackageName));
managerRecord.startScan();
}
@@ -1457,8 +1453,13 @@
return;
}
- Slog.i(TAG, TextUtils.formatSimple(
- "stopScan | manager: %d", managerRecord.mManagerId));
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "stopScan | manager: %d, ownerPackageName: %s, targetPackageName: %s",
+ managerRecord.mManagerId,
+ managerRecord.mOwnerPackageName,
+ managerRecord.mTargetPackageName));
managerRecord.stopScan();
}
@@ -1725,13 +1726,6 @@
// End of locked methods that are used by both MediaRouter2 and MediaRouter2Manager.
- private void onDeviceConfigChange(@NonNull DeviceConfig.Properties properties) {
- sPackageImportanceForScanning =
- properties.getInt(
- /* name */ FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE,
- /* defaultValue */ IMPORTANCE_FOREGROUND_SERVICE);
- }
-
static long toUniqueRequestId(int requesterId, int originalRequestId) {
return ((long) requesterId << 32) | originalRequestId;
}
@@ -3170,7 +3164,7 @@
record ->
service.mActivityManager.getPackageImportance(
record.mPackageName)
- <= sPackageImportanceForScanning)
+ <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING)
.collect(Collectors.toList());
}
@@ -3187,7 +3181,7 @@
manager.mIsScanning
&& service.mActivityManager.getPackageImportance(
manager.mOwnerPackageName)
- <= sPackageImportanceForScanning);
+ <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING);
}
private MediaRoute2Provider findProvider(@Nullable String providerId) {
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index f6571d9..550aed5 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -304,7 +304,7 @@
}
@VisibleForTesting
- void addCallback(final IMediaProjectionWatcherCallback callback) {
+ MediaProjectionInfo addCallback(final IMediaProjectionWatcherCallback callback) {
IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
@@ -314,6 +314,7 @@
synchronized (mLock) {
mCallbackDelegate.add(callback);
linkDeathRecipientLocked(callback, deathRecipient);
+ return mProjectionGrant != null ? mProjectionGrant.getProjectionInfo() : null;
}
}
@@ -786,11 +787,11 @@
@Override //Binder call
@EnforcePermission(MANAGE_MEDIA_PROJECTION)
- public void addCallback(final IMediaProjectionWatcherCallback callback) {
+ public MediaProjectionInfo addCallback(final IMediaProjectionWatcherCallback callback) {
addCallback_enforcePermission();
final long token = Binder.clearCallingIdentity();
try {
- MediaProjectionManagerService.this.addCallback(callback);
+ return MediaProjectionManagerService.this.addCallback(callback);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1244,7 +1245,7 @@
}
public MediaProjectionInfo getProjectionInfo() {
- return new MediaProjectionInfo(packageName, userHandle);
+ return new MediaProjectionInfo(packageName, userHandle, mLaunchCookie);
}
boolean requiresForegroundService() {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 9ddc362..2ae040a6 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -5931,8 +5931,7 @@
newVisualEffects, policy.priorityConversationSenders);
if (shouldApplyAsImplicitRule) {
- mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy,
- origin);
+ mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy);
} else {
ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion,
policy);
@@ -12103,6 +12102,7 @@
return true;
}
+ long token = Binder.clearCallingIdentity();
try {
if (mPackageManager.checkUidPermission(RECEIVE_SENSITIVE_NOTIFICATIONS, uid)
== PERMISSION_GRANTED || mPackageManagerInternal.isPlatformSigned(pkg)) {
@@ -12129,6 +12129,8 @@
}
} catch (RemoteException e) {
Slog.e(TAG, "Failed to check trusted status of listener", e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
return false;
}
diff --git a/services/core/java/com/android/server/notification/ZenAdapters.java b/services/core/java/com/android/server/notification/ZenAdapters.java
index 91df04c..37b263c 100644
--- a/services/core/java/com/android/server/notification/ZenAdapters.java
+++ b/services/core/java/com/android/server/notification/ZenAdapters.java
@@ -59,9 +59,7 @@
}
if (Flags.modesApi()) {
- zenPolicyBuilder.allowChannels(
- policy.allowPriorityChannels()
- ? ZenPolicy.CHANNEL_TYPE_PRIORITY : ZenPolicy.CHANNEL_TYPE_NONE);
+ zenPolicyBuilder.allowPriorityChannels(policy.allowPriorityChannels());
}
return zenPolicyBuilder.build();
diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
index 0145577..a90efe6 100644
--- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java
+++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
@@ -18,6 +18,8 @@
import static android.app.NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND;
import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_PRIORITY;
+import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_NONE;
import static android.service.notification.NotificationServiceProto.RULE_TYPE_AUTOMATIC;
import static android.service.notification.NotificationServiceProto.RULE_TYPE_MANUAL;
import static android.service.notification.NotificationServiceProto.RULE_TYPE_UNKNOWN;
@@ -551,8 +553,8 @@
if (Flags.modesApi()) {
proto.write(DNDPolicyProto.ALLOW_CHANNELS,
mNewPolicy.allowPriorityChannels()
- ? ZenPolicy.CHANNEL_TYPE_PRIORITY
- : ZenPolicy.CHANNEL_TYPE_NONE);
+ ? CHANNEL_POLICY_PRIORITY
+ : CHANNEL_POLICY_NONE);
}
} else {
Log.wtf(TAG, "attempted to write zen mode log event with null policy");
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index afbf08d..93ffd97 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -32,6 +32,7 @@
import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER;
import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
+import static com.android.internal.util.Preconditions.checkArgument;
import android.annotation.DrawableRes;
import android.annotation.NonNull;
@@ -427,6 +428,7 @@
public String addAutomaticZenRule(String pkg, AutomaticZenRule automaticZenRule,
@ConfigChangeOrigin int origin, String reason, int callingUid) {
+ requirePublicOrigin("addAutomaticZenRule", origin);
if (!ZenModeConfig.SYSTEM_AUTHORITY.equals(pkg)) {
PackageItemInfo component = getServiceInfo(automaticZenRule.getOwner());
if (component == null) {
@@ -525,6 +527,7 @@
public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule,
@ConfigChangeOrigin int origin, String reason, int callingUid) {
+ requirePublicOrigin("updateAutomaticZenRule", origin);
ZenModeConfig newConfig;
synchronized (mConfigLock) {
if (mConfig == null) return false;
@@ -602,7 +605,11 @@
rule = newImplicitZenRule(callingPkg);
newConfig.automaticRules.put(rule.id, rule);
}
- rule.zenMode = zenMode;
+ // If the user has changed the rule's *zenMode*, then don't let app overwrite it.
+ // We allow the update if the user has only changed other aspects of the rule.
+ if ((rule.userModifiedFields & AutomaticZenRule.FIELD_INTERRUPTION_FILTER) == 0) {
+ rule.zenMode = zenMode;
+ }
rule.snoozing = false;
rule.condition = new Condition(rule.conditionId,
mContext.getString(R.string.zen_mode_implicit_activated),
@@ -625,7 +632,7 @@
* {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}.
*/
void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid,
- NotificationManager.Policy policy, @ConfigChangeOrigin int origin) {
+ NotificationManager.Policy policy) {
if (!android.app.Flags.modesApi()) {
Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!");
return;
@@ -641,10 +648,17 @@
rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
newConfig.automaticRules.put(rule.id, rule);
}
- // TODO: b/308673679 - Keep user customization of this rule!
- rule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy);
- setConfigLocked(newConfig, /* triggeringComponent= */ null, origin,
- "applyGlobalPolicyAsImplicitZenRule", callingUid);
+ // If the user has changed the rule's *ZenPolicy*, then don't let app overwrite it.
+ // We allow the update if the user has only changed other aspects of the rule.
+ if (rule.zenPolicyUserModifiedFields == 0) {
+ updatePolicy(
+ rule,
+ ZenAdapters.notificationPolicyToZenPolicy(policy),
+ /* updateBitmask= */ false);
+
+ setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP,
+ "applyGlobalPolicyAsImplicitZenRule", callingUid);
+ }
}
}
@@ -726,6 +740,7 @@
boolean removeAutomaticZenRule(String id, @ConfigChangeOrigin int origin, String reason,
int callingUid) {
+ requirePublicOrigin("removeAutomaticZenRule", origin);
ZenModeConfig newConfig;
synchronized (mConfigLock) {
if (mConfig == null) return false;
@@ -758,6 +773,7 @@
boolean removeAutomaticZenRules(String packageName, @ConfigChangeOrigin int origin,
String reason, int callingUid) {
+ requirePublicOrigin("removeAutomaticZenRules", origin);
ZenModeConfig newConfig;
synchronized (mConfigLock) {
if (mConfig == null) return false;
@@ -806,6 +822,7 @@
void setAutomaticZenRuleState(String id, Condition condition, @ConfigChangeOrigin int origin,
int callingUid) {
+ requirePublicOrigin("setAutomaticZenRuleState", origin);
ZenModeConfig newConfig;
synchronized (mConfigLock) {
if (mConfig == null) return;
@@ -819,6 +836,7 @@
void setAutomaticZenRuleState(Uri ruleDefinition, Condition condition,
@ConfigChangeOrigin int origin, int callingUid) {
+ requirePublicOrigin("setAutomaticZenRuleState", origin);
ZenModeConfig newConfig;
synchronized (mConfigLock) {
if (mConfig == null) return;
@@ -988,7 +1006,7 @@
return null;
}
- void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
+ private void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
@ConfigChangeOrigin int origin, boolean isNew) {
if (Flags.modesApi()) {
// These values can always be edited by the app, so we apply changes immediately.
@@ -1053,11 +1071,9 @@
rule.zenMode = newZenMode;
// Updates the bitmask and values for all policy fields, based on the origin.
- rule.zenPolicy = updatePolicy(rule.zenPolicy, automaticZenRule.getZenPolicy(),
- updateBitmask);
+ updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask);
// Updates the bitmask and values for all device effect fields, based on the origin.
- rule.zenDeviceEffects = updateZenDeviceEffects(
- rule.zenDeviceEffects, automaticZenRule.getDeviceEffects(),
+ updateZenDeviceEffects(rule, automaticZenRule.getDeviceEffects(),
origin == UPDATE_ORIGIN_APP, updateBitmask);
} else {
if (rule.enabled != automaticZenRule.isEnabled()) {
@@ -1069,13 +1085,6 @@
rule.enabled = automaticZenRule.isEnabled();
rule.modified = automaticZenRule.isModified();
rule.zenPolicy = automaticZenRule.getZenPolicy();
- if (Flags.modesApi()) {
- rule.zenDeviceEffects = updateZenDeviceEffects(
- rule.zenDeviceEffects,
- automaticZenRule.getDeviceEffects(),
- origin == UPDATE_ORIGIN_APP,
- origin == UPDATE_ORIGIN_USER);
- }
rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
rule.configurationActivity = automaticZenRule.getConfigurationActivity();
@@ -1099,28 +1108,28 @@
}
/**
- * Modifies {@link ZenPolicy} that is being stored as part of a new or updated ZenRule.
- * Returns a policy based on {@code oldPolicy}, but with fields updated to match
- * {@code newPolicy} where they differ, and updating the internal user-modified bitmask to
- * track these changes, if applicable based on {@code origin}.
+ * Modifies the {@link ZenPolicy} associated to a new or updated ZenRule.
+ *
+ * <p>The new policy is {@code newPolicy}, while the user-modified bitmask is updated to reflect
+ * the changes being applied (if applicable, i.e. if the update is from the user).
*/
- @Nullable
- private ZenPolicy updatePolicy(@Nullable ZenPolicy oldPolicy, @Nullable ZenPolicy newPolicy,
- boolean updateBitmask) {
- // If the update is to make the policy null, we don't need to update the bitmask,
- // because it won't be stored anywhere anyway.
+ private void updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy,
+ boolean updateBitmask) {
if (newPolicy == null) {
- return null;
+ // TODO: b/319242206 - Treat as newPolicy == default policy and continue below.
+ zenRule.zenPolicy = null;
+ return;
}
// If oldPolicy is null, we compare against the default policy when determining which
// fields in the bitmask should be marked as updated.
- if (oldPolicy == null) {
- oldPolicy = mDefaultConfig.toZenPolicy();
- }
+ ZenPolicy oldPolicy =
+ zenRule.zenPolicy != null ? zenRule.zenPolicy : mDefaultConfig.toZenPolicy();
- int userModifiedFields = oldPolicy.getUserModifiedFields();
+ zenRule.zenPolicy = newPolicy;
+
if (updateBitmask) {
+ int userModifiedFields = zenRule.zenPolicyUserModifiedFields;
if (oldPolicy.getPriorityMessageSenders() != newPolicy.getPriorityMessageSenders()) {
userModifiedFields |= ZenPolicy.FIELD_MESSAGES;
}
@@ -1131,7 +1140,7 @@
!= newPolicy.getPriorityConversationSenders()) {
userModifiedFields |= ZenPolicy.FIELD_CONVERSATIONS;
}
- if (oldPolicy.getAllowedChannels() != newPolicy.getAllowedChannels()) {
+ if (oldPolicy.getPriorityChannels() != newPolicy.getPriorityChannels()) {
userModifiedFields |= ZenPolicy.FIELD_ALLOW_CHANNELS;
}
if (oldPolicy.getPriorityCategoryReminders()
@@ -1178,66 +1187,47 @@
!= newPolicy.getVisualEffectNotificationList()) {
userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_NOTIFICATION_LIST;
}
+ zenRule.zenPolicyUserModifiedFields = userModifiedFields;
}
-
- // After all bitmask changes have been made, sets the bitmask.
- return new ZenPolicy.Builder(newPolicy).setUserModifiedFields(userModifiedFields).build();
}
/**
- * Modifies {@link ZenDeviceEffects} that are being stored as part of a new or updated ZenRule.
- * Returns a {@link ZenDeviceEffects} based on {@code oldEffects}, but with fields updated to
- * match {@code newEffects} where they differ, and updating the internal user-modified bitmask
- * to track these changes, if applicable based on {@code origin}.
- * <ul>
- * <li> Apps cannot turn on hidden effects (those tagged as {@code @hide}) since they are
- * intended for platform-specific rules (e.g. wearables). If it's a new rule, we blank them
- * out; if it's an update, we preserve the previous values.
- * </ul>
+ * Modifies the {@link ZenDeviceEffects} associated to a new or updated ZenRule.
+ *
+ * <p>The new value is {@code newEffects}, while the user-modified bitmask is updated to reflect
+ * the changes being applied (if applicable, i.e. if the update is from the user).
+ *
+ * <p>Apps cannot turn on hidden effects (those tagged as {@code @hide}), so those fields are
+ * treated especially: for a new rule, they are blanked out; for an updated rule, previous
+ * values are preserved.
*/
- @Nullable
- private static ZenDeviceEffects updateZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects,
- @Nullable ZenDeviceEffects newEffects,
- boolean isFromApp,
- boolean updateBitmask) {
+ private static void updateZenDeviceEffects(ZenRule zenRule,
+ @Nullable ZenDeviceEffects newEffects, boolean isFromApp, boolean updateBitmask) {
if (newEffects == null) {
- return null;
+ zenRule.zenDeviceEffects = null;
+ return;
}
- // Since newEffects is not null, we want to adopt all the new provided device effects.
- ZenDeviceEffects.Builder builder = new ZenDeviceEffects.Builder(newEffects);
+ ZenDeviceEffects oldEffects = zenRule.zenDeviceEffects != null
+ ? zenRule.zenDeviceEffects
+ : new ZenDeviceEffects.Builder().build();
if (isFromApp) {
- if (oldEffects != null) {
- // We can do this because we know we don't need to update the bitmask FROM_APP.
- return builder
- .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness())
- .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake())
- .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake())
- .setShouldDisableTouch(oldEffects.shouldDisableTouch())
- .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage())
- .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze())
- .build();
- } else {
- return builder
- .setShouldDisableAutoBrightness(false)
- .setShouldDisableTapToWake(false)
- .setShouldDisableTiltToWake(false)
- .setShouldDisableTouch(false)
- .setShouldMinimizeRadioUsage(false)
- .setShouldMaximizeDoze(false)
- .build();
- }
+ // Don't allow apps to toggle hidden effects.
+ newEffects = new ZenDeviceEffects.Builder(newEffects)
+ .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness())
+ .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake())
+ .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake())
+ .setShouldDisableTouch(oldEffects.shouldDisableTouch())
+ .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage())
+ .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze())
+ .build();
}
- // If oldEffects is null, we compare against the default device effects object when
- // determining which fields in the bitmask should be marked as updated.
- if (oldEffects == null) {
- oldEffects = new ZenDeviceEffects.Builder().build();
- }
+ zenRule.zenDeviceEffects = newEffects;
- int userModifiedFields = oldEffects.getUserModifiedFields();
if (updateBitmask) {
+ int userModifiedFields = zenRule.zenDeviceEffectsUserModifiedFields;
if (oldEffects.shouldDisplayGrayscale() != newEffects.shouldDisplayGrayscale()) {
userModifiedFields |= ZenDeviceEffects.FIELD_GRAYSCALE;
}
@@ -1270,11 +1260,8 @@
if (oldEffects.shouldMaximizeDoze() != newEffects.shouldMaximizeDoze()) {
userModifiedFields |= ZenDeviceEffects.FIELD_MAXIMIZE_DOZE;
}
+ zenRule.zenDeviceEffectsUserModifiedFields = userModifiedFields;
}
-
- // Since newEffects is not null, we want to adopt all the new provided device effects.
- // Set the usermodifiedFields value separately, to reflect the updated bitmask.
- return builder.setUserModifiedFields(userModifiedFields).build();
}
private AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
@@ -1293,7 +1280,6 @@
.setOwner(rule.component)
.setConfigurationActivity(rule.configurationActivity)
.setTriggerDescription(rule.triggerDescription)
- .setUserModifiedFields(rule.userModifiedFields)
.build();
} else {
azr = new AutomaticZenRule(rule.name, rule.component,
@@ -2369,6 +2355,19 @@
return null;
}
}
+
+ /** Checks that the {@code origin} supplied to a ZenModeHelper "API" method makes sense. */
+ private static void requirePublicOrigin(String method, @ConfigChangeOrigin int origin) {
+ if (!Flags.modesApi()) {
+ return;
+ }
+ checkArgument(origin == UPDATE_ORIGIN_APP || origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI
+ || origin == UPDATE_ORIGIN_USER,
+ "Expected one of UPDATE_ORIGIN_APP, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, or "
+ + "UPDATE_ORIGIN_USER for %s, but received '%s'.",
+ method, origin);
+ }
+
private final class Metrics extends Callback {
private static final String COUNTER_MODE_PREFIX = "dnd_mode_";
private static final String COUNTER_TYPE_PREFIX = "dnd_type_";
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index 659c36c..5d71439e 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -276,7 +276,8 @@
* Returns list of {@code packageName} of apks inside the given apex.
* @param apexPackageName Package name of the apk container of apex
*/
- abstract List<String> getApksInApex(String apexPackageName);
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public abstract List<String> getApksInApex(String apexPackageName);
/**
* Returns the apex module name for the given package name, if the package is an APEX. Otherwise
@@ -751,8 +752,9 @@
}
}
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@Override
- List<String> getApksInApex(String apexPackageName) {
+ public List<String> getApksInApex(String apexPackageName) {
synchronized (mLock) {
Preconditions.checkState(mPackageNameToApexModuleName != null,
"APEX packages have not been scanned");
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index 7f0aadc..c110fb6 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -75,11 +75,9 @@
private static final int MSG_PACKAGE_ADDED = 1;
private static final int MSG_PACKAGE_REMOVED = 2;
- private final Context mContext;
private final BinderService mBinderService;
private final PackageManager mPackageManager;
private final PackageManagerInternal mPackageManagerInternal;
- private final UsageStatsManagerInternal mUsageStatsManagerInternal;
private final PermissionManagerServiceInternal mPermissionManager;
private final Handler mHandler;
private final File mDiskFile;
@@ -99,14 +97,14 @@
@VisibleForTesting
BackgroundInstallControlService(@NonNull Injector injector) {
super(injector.getContext());
- mContext = injector.getContext();
mPackageManager = injector.getPackageManager();
mPackageManagerInternal = injector.getPackageManagerInternal();
mPermissionManager = injector.getPermissionManager();
mHandler = new EventHandler(injector.getLooper(), this);
mDiskFile = injector.getDiskFile();
- mUsageStatsManagerInternal = injector.getUsageStatsManagerInternal();
- mUsageStatsManagerInternal.registerListener(
+ UsageStatsManagerInternal usageStatsManagerInternal =
+ injector.getUsageStatsManagerInternal();
+ usageStatsManagerInternal.registerListener(
(userId, event) ->
mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED,
userId,
diff --git a/services/core/java/com/android/server/pm/DataLoaderManagerService.java b/services/core/java/com/android/server/pm/DataLoaderManagerService.java
index 888e1c2..c25cea6 100644
--- a/services/core/java/com/android/server/pm/DataLoaderManagerService.java
+++ b/services/core/java/com/android/server/pm/DataLoaderManagerService.java
@@ -47,7 +47,6 @@
public class DataLoaderManagerService extends SystemService {
private static final String TAG = "DataLoaderManager";
private final Context mContext;
- private final HandlerThread mThread;
private final Handler mHandler;
private final DataLoaderManagerBinderService mBinderService;
private final SparseArray<DataLoaderServiceConnection> mServiceConnections =
@@ -57,10 +56,10 @@
super(context);
mContext = context;
- mThread = new HandlerThread(TAG);
- mThread.start();
+ HandlerThread thread = new HandlerThread(TAG);
+ thread.start();
- mHandler = new Handler(mThread.getLooper());
+ mHandler = new Handler(thread.getLooper());
mBinderService = new DataLoaderManagerBinderService();
}
diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java
index e3bbd2d..f987d4a 100644
--- a/services/core/java/com/android/server/pm/IPackageManagerBase.java
+++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java
@@ -107,9 +107,6 @@
@Nullable
private final ComponentName mInstantAppResolverSettingsComponent;
- @NonNull
- private final String mRequiredSupplementalProcessPackage;
-
@Nullable
private final String mServicesExtensionPackageName;
@@ -125,7 +122,6 @@
@NonNull PackageInstallerService installerService,
@NonNull PackageProperty packageProperty, @NonNull ComponentName resolveComponentName,
@Nullable ComponentName instantAppResolverSettingsComponent,
- @NonNull String requiredSupplementalProcessPackage,
@Nullable String servicesExtensionPackageName,
@Nullable String sharedSystemSharedLibraryPackageName) {
mService = service;
@@ -140,7 +136,6 @@
mPackageProperty = packageProperty;
mResolveComponentName = resolveComponentName;
mInstantAppResolverSettingsComponent = instantAppResolverSettingsComponent;
- mRequiredSupplementalProcessPackage = requiredSupplementalProcessPackage;
mServicesExtensionPackageName = servicesExtensionPackageName;
mSharedSystemSharedLibraryPackageName = sharedSystemSharedLibraryPackageName;
}
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index d5471cb0..34903d1 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -1183,8 +1183,7 @@
* Returns an auth token for the provided writable FD.
*
* @param authFd a file descriptor to proof that the caller can write to the file.
- * @param appUid uid of the calling app.
- * @param userId id of the user whose app file to enable fs-verity.
+ * @param uid uid of the calling app.
*
* @return authToken, or null if a remote call shouldn't be continued. See {@link
* #checkBeforeRemote}.
@@ -1192,13 +1191,12 @@
* @throws InstallerException if the remote call failed.
*/
public IInstalld.IFsveritySetupAuthToken createFsveritySetupAuthToken(
- ParcelFileDescriptor authFd, int appUid, @UserIdInt int userId)
- throws InstallerException {
+ ParcelFileDescriptor authFd, int uid) throws InstallerException {
if (!checkBeforeRemote()) {
return null;
}
try {
- return mInstalld.createFsveritySetupAuthToken(authFd, appUid, userId);
+ return mInstalld.createFsveritySetupAuthToken(authFd, uid);
} catch (Exception e) {
throw InstallerException.from(e);
}
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 3e5759a..b18f2bf 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -51,6 +51,7 @@
import android.content.IntentSender;
import android.content.pm.ApplicationInfo;
import android.content.pm.ArchivedActivityParcel;
+import android.content.pm.ArchivedPackageInfo;
import android.content.pm.ArchivedPackageParcel;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
@@ -402,23 +403,30 @@
installerPackage, /* flags= */ 0, userId);
if (installerInfo == null) {
// Should never happen because we just fetched the installerInfo.
- Slog.e(TAG, "Couldnt find installer " + installerPackage);
+ Slog.e(TAG, "Couldn't find installer " + installerPackage);
return null;
}
+ final int iconSize = mContext.getSystemService(
+ ActivityManager.class).getLauncherLargeIconSize();
+
+ var info = new ArchivedPackageInfo(archivedPackage);
try {
- var packageName = archivedPackage.packageName;
- var mainActivities = archivedPackage.archivedActivities;
- List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.length);
- for (int i = 0, size = mainActivities.length; i < size; ++i) {
- var mainActivity = mainActivities[i];
- Path iconPath = storeIconForParcel(packageName, mainActivity, userId, i);
+ var packageName = info.getPackageName();
+ var mainActivities = info.getLauncherActivities();
+ List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size());
+ for (int i = 0, size = mainActivities.size(); i < size; ++i) {
+ var mainActivity = mainActivities.get(i);
+ Path iconPath = storeDrawable(
+ packageName, mainActivity.getIcon(), userId, i, iconSize);
+ Path monochromePath = storeDrawable(
+ packageName, mainActivity.getMonochromeIcon(), userId, i, iconSize);
ArchiveActivityInfo activityInfo =
new ArchiveActivityInfo(
- mainActivity.title,
- mainActivity.originalComponentName,
+ mainActivity.getLabel().toString(),
+ mainActivity.getComponentName(),
iconPath,
- null);
+ monochromePath);
archiveActivityInfos.add(activityInfo);
}
@@ -452,21 +460,6 @@
return new ArchiveState(archiveActivityInfos, installerTitle);
}
- // TODO(b/298452477) Handle monochrome icons.
- private static Path storeIconForParcel(String packageName, ArchivedActivityParcel mainActivity,
- @UserIdInt int userId, int index) throws IOException {
- if (mainActivity.iconBitmap == null) {
- return null;
- }
- File iconsDir = createIconsDir(packageName, userId);
- File iconFile = new File(iconsDir, index + ".png");
- try (FileOutputStream out = new FileOutputStream(iconFile)) {
- out.write(mainActivity.iconBitmap);
- out.flush();
- }
- return iconFile.toPath();
- }
-
@VisibleForTesting
Path storeIcon(String packageName, LauncherActivityInfo mainActivity,
@UserIdInt int userId, int index, int iconSize) throws IOException {
@@ -475,9 +468,18 @@
// The app doesn't define an icon. No need to store anything.
return null;
}
+ return storeDrawable(packageName, mainActivity.getIcon(/* density= */ 0), userId, index,
+ iconSize);
+ }
+
+ private static Path storeDrawable(String packageName, @Nullable Drawable iconDrawable,
+ @UserIdInt int userId, int index, int iconSize) throws IOException {
+ if (iconDrawable == null) {
+ return null;
+ }
File iconsDir = createIconsDir(packageName, userId);
File iconFile = new File(iconsDir, index + ".png");
- Bitmap icon = drawableToBitmap(mainActivity.getIcon(/* density= */ 0), iconSize);
+ Bitmap icon = drawableToBitmap(iconDrawable, iconSize);
try (FileOutputStream out = new FileOutputStream(iconFile)) {
// Note: Quality is ignored for PNGs.
if (!icon.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, out)) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 5225529..c5b5a76 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -4659,8 +4659,7 @@
mPreferredActivityHelper, mResolveIntentHelper, mDomainVerificationManager,
mDomainVerificationConnection, mInstallerService, mPackageProperty,
mResolveComponentName, mInstantAppResolverSettingsComponent,
- mRequiredSdkSandboxPackage, mServicesExtensionPackageName,
- mSharedSystemSharedLibraryPackageName);
+ mServicesExtensionPackageName, mSharedSystemSharedLibraryPackageName);
}
@Override
diff --git a/services/core/java/com/android/server/pm/ProtectedPackages.java b/services/core/java/com/android/server/pm/ProtectedPackages.java
index 98533725..524252c 100644
--- a/services/core/java/com/android/server/pm/ProtectedPackages.java
+++ b/services/core/java/com/android/server/pm/ProtectedPackages.java
@@ -57,11 +57,8 @@
@GuardedBy("this")
private final SparseArray<Set<String>> mOwnerProtectedPackages = new SparseArray<>();
- private final Context mContext;
-
public ProtectedPackages(Context context) {
- mContext = context;
- mDeviceProvisioningPackage = mContext.getResources().getString(
+ mDeviceProvisioningPackage = context.getResources().getString(
R.string.config_deviceProvisioningPackage);
}
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 7bd6a43..3e3b72c 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -67,7 +67,6 @@
private final PackageManagerService mPm;
private final IncrementalManager mIncrementalManager;
private final Installer mInstaller;
- private final UserManagerInternal mUserManagerInternal;
private final PermissionManagerServiceInternal mPermissionManager;
private final SharedLibrariesImpl mSharedLibraries;
private final AppDataHelper mAppDataHelper;
@@ -79,7 +78,6 @@
mPm = pm;
mIncrementalManager = mPm.mInjector.getIncrementalManager();
mInstaller = mPm.mInjector.getInstaller();
- mUserManagerInternal = mPm.mInjector.getUserManagerInternal();
mPermissionManager = mPm.mInjector.getPermissionManagerServiceInternal();
mSharedLibraries = mPm.mInjector.getSharedLibrariesImpl();
mAppDataHelper = appDataHelper;
diff --git a/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java b/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java
index 7f6f684..aa52522 100644
--- a/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java
@@ -31,7 +31,6 @@
* The access to it must be guarded with the shortcut manager lock.
*/
public class ShortcutNonPersistentUser {
- private final ShortcutService mService;
private final int mUserId;
@@ -49,8 +48,7 @@
*/
private final ArraySet<String> mHostPackageSet = new ArraySet<>();
- public ShortcutNonPersistentUser(ShortcutService service, int userId) {
- mService = service;
+ public ShortcutNonPersistentUser(int userId) {
mUserId = userId;
}
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 446c629..96c205c 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -1378,7 +1378,7 @@
ShortcutNonPersistentUser getNonPersistentUserLocked(@UserIdInt int userId) {
ShortcutNonPersistentUser ret = mShortcutNonPersistentUsers.get(userId);
if (ret == null) {
- ret = new ShortcutNonPersistentUser(this, userId);
+ ret = new ShortcutNonPersistentUser(userId);
mShortcutNonPersistentUsers.put(userId, ret);
}
return ret;
diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java
index 4c42c2d..1d41401 100644
--- a/services/core/java/com/android/server/pm/UserDataPreparer.java
+++ b/services/core/java/com/android/server/pm/UserDataPreparer.java
@@ -141,7 +141,7 @@
// If internal storage of the system user fails to prepare on first boot, then
// things are *really* broken, so we might as well reboot to recovery right away.
try {
- Log.wtf(TAG, "prepareUserData failed for user " + userId, e);
+ Log.e(TAG, "prepareUserData failed for user " + userId, e);
if (isNewUser && userId == UserHandle.USER_SYSTEM && volumeUuid == null) {
RecoverySystem.rebootPromptAndWipeUserData(mContext,
"failed to prepare internal storage for system user");
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index c1b7489..7b0a69b 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -46,6 +46,7 @@
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityManagerNative;
+import android.app.ActivityOptions;
import android.app.BroadcastOptions;
import android.app.IActivityManager;
import android.app.IStopUserCallback;
@@ -587,7 +588,10 @@
public void onFinished(int id, Bundle extras) {
mHandler.post(() -> {
try {
- mContext.startIntentSender(mTarget, null, 0, 0, 0);
+ ActivityOptions activityOptions =
+ ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ mContext.startIntentSender(mTarget, null, 0, 0, 0, activityOptions.toBundle());
} catch (IntentSender.SendIntentException e) {
Slog.e(LOG_TAG, "Failed to start the target in the callback", e);
}
diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java
index c6435ae..f0ff85d 100644
--- a/services/core/java/com/android/server/pm/VerifyingSession.java
+++ b/services/core/java/com/android/server/pm/VerifyingSession.java
@@ -356,6 +356,11 @@
if (verifierUser == UserHandle.ALL) {
verifierUser = UserHandle.of(mPm.mUserManager.getCurrentUserId());
}
+ // TODO(b/300965895): Remove when inconsistencies loading classpaths from apex for
+ // user > 1 are fixed.
+ if (pkgLite.isSdkLibrary) {
+ verifierUser = UserHandle.SYSTEM;
+ }
final int verifierUserId = verifierUser.getIdentifier();
List<String> requiredVerifierPackages = new ArrayList<>(
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 5d710d2..40f2264 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -29,6 +29,7 @@
import static android.app.AppOpsManager.OP_BLUETOOTH_CONNECT;
import static android.content.pm.ApplicationInfo.AUTO_REVOKE_DISALLOWED;
import static android.content.pm.ApplicationInfo.AUTO_REVOKE_DISCOURAGED;
+import static android.permission.flags.Flags.serverSideAttributionRegistration;
import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
@@ -76,7 +77,6 @@
import com.android.internal.util.function.QuadFunction;
import com.android.internal.util.function.TriFunction;
import com.android.server.LocalServices;
-import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.UserManagerService;
import com.android.server.pm.permission.PermissionManagerServiceInternal.HotwordDetectionServiceProvider;
import com.android.server.pm.pkg.AndroidPackage;
@@ -113,9 +113,6 @@
/** Internal connection to the package manager */
private final PackageManagerInternal mPackageManagerInt;
- /** Internal connection to the user manager */
- private final UserManagerInternal mUserManagerInt;
-
/** Map of OneTimePermissionUserManagers keyed by userId */
@GuardedBy("mLock")
@NonNull
@@ -147,7 +144,6 @@
mContext = context;
mPackageManagerInt = LocalServices.getService(PackageManagerInternal.class);
- mUserManagerInt = LocalServices.getService(UserManagerInternal.class);
mAppOpsManager = context.getSystemService(AppOpsManager.class);
mAttributionSourceRegistry = new AttributionSourceRegistry(context);
@@ -439,10 +435,27 @@
}
}
+ /**
+ * Reference propagation over binder is affected by the ownership of the object. So if
+ * the token is owned by client, references to the token on client side won't be
+ * propagated to the server and the token may still be garbage collected on server side.
+ * But if the token is owned by server, references to the token on client side will now
+ * be propagated to the server since it's a foreign object to the client, and that will
+ * keep the token referenced on the server side as long as the client is alive and
+ * holding it.
+ */
@Override
- public void registerAttributionSource(@NonNull AttributionSourceState source) {
- mAttributionSourceRegistry
- .registerAttributionSource(new AttributionSource(source));
+ public IBinder registerAttributionSource(@NonNull AttributionSourceState source) {
+ if (serverSideAttributionRegistration()) {
+ Binder token = new Binder();
+ mAttributionSourceRegistry
+ .registerAttributionSource(new AttributionSource(source).withToken(token));
+ return token;
+ } else {
+ mAttributionSourceRegistry
+ .registerAttributionSource(new AttributionSource(source));
+ return source.token;
+ }
}
@Override
@@ -1080,12 +1093,10 @@
private static final AtomicInteger sAttributionChainIds = new AtomicInteger(0);
private final @NonNull Context mContext;
- private final @NonNull AppOpsManager mAppOpsManager;
private final @NonNull PermissionManagerServiceInternal mPermissionManagerServiceInternal;
PermissionCheckerService(@NonNull Context context) {
mContext = context;
- mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
mPermissionManagerServiceInternal =
LocalServices.getService(PermissionManagerServiceInternal.class);
}
@@ -1218,7 +1229,6 @@
@Nullable String message, boolean forDataDelivery, boolean startDataDelivery,
boolean fromDatasource, int attributedOp) {
PermissionInfo permissionInfo = sPlatformPermissions.get(permission);
-
if (permissionInfo == null) {
try {
permissionInfo = context.getPackageManager().getPermissionInfo(permission, 0);
@@ -1346,8 +1356,8 @@
// If the call is from a datasource we need to vet only the chain before it. This
// way we can avoid the datasource creating an attribution context for every call.
- if (!(fromDatasource && current.equals(attributionSource))
- && next != null && !current.isTrusted(context)) {
+ boolean isDatasource = fromDatasource && current.equals(attributionSource);
+ if (!isDatasource && next != null && !current.isTrusted(context)) {
return PermissionChecker.PERMISSION_HARD_DENIED;
}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 3afba39..6a57362 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -279,7 +279,6 @@
@NonNull
private final int[] mGlobalGids;
- private final HandlerThread mHandlerThread;
private final Handler mHandler;
private final Context mContext;
private final MetricsLogger mMetricsLogger = new MetricsLogger();
@@ -432,10 +431,10 @@
}
}
- mHandlerThread = new ServiceThread(TAG,
+ HandlerThread handlerThread = new ServiceThread(TAG,
Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
- mHandlerThread.start();
- mHandler = new Handler(mHandlerThread.getLooper());
+ handlerThread.start();
+ mHandler = new Handler(handlerThread.getLooper());
Watchdog.getInstance().addThread(mHandler);
SystemConfig systemConfig = SystemConfig.getInstance();
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index e226953..a172de0 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -4413,8 +4413,8 @@
private boolean setPowerModeInternal(int mode, boolean enabled) {
// Maybe filter the event.
- if (mBatterySaverStateMachine == null || (mode == Mode.LAUNCH && enabled
- && mBatterySaverStateMachine.getBatterySaverController().isLaunchBoostDisabled())) {
+ if (mode == Mode.LAUNCH && enabled && mBatterySaverStateMachine != null
+ && mBatterySaverStateMachine.getBatterySaverController().isLaunchBoostDisabled()) {
return false;
}
return mNativeWrapper.nativeSetPowerMode(mode, enabled);
diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java
index a49df50..bb4876b 100644
--- a/services/core/java/com/android/server/security/FileIntegrityService.java
+++ b/services/core/java/com/android/server/security/FileIntegrityService.java
@@ -157,7 +157,7 @@
Objects.requireNonNull(authFd);
try {
var authToken = getStorageManagerInternal().createFsveritySetupAuthToken(authFd,
- Binder.getCallingUid(), Binder.getCallingUserHandle().getIdentifier());
+ Binder.getCallingUid());
// fs-verity setup requires no writable fd to the file. Release the dup now that
// it's passed.
authFd.close();
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index 0467d0c..7ab075e 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -38,7 +38,16 @@
import android.media.tv.BroadcastInfoResponse;
import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
+import android.media.tv.ad.ITvAdClient;
import android.media.tv.ad.ITvAdManager;
+import android.media.tv.ad.ITvAdManagerCallback;
+import android.media.tv.ad.ITvAdService;
+import android.media.tv.ad.ITvAdServiceCallback;
+import android.media.tv.ad.ITvAdSession;
+import android.media.tv.ad.ITvAdSessionCallback;
+import android.media.tv.ad.TvAdService;
+import android.media.tv.ad.TvAdServiceInfo;
+import android.media.tv.flags.Flags;
import android.media.tv.interactive.AppLinkInfo;
import android.media.tv.interactive.ITvInteractiveAppClient;
import android.media.tv.interactive.ITvInteractiveAppManager;
@@ -110,6 +119,8 @@
@GuardedBy("mLock")
private boolean mGetServiceListCalled = false;
@GuardedBy("mLock")
+ private boolean mGetAdServiceListCalled = false;
+ @GuardedBy("mLock")
private boolean mGetAppLinkInfoListCalled = false;
private final UserManager mUserManager;
@@ -256,6 +267,141 @@
}
@GuardedBy("mLock")
+ private void buildTvAdServiceListLocked(int userId, String[] updatedPackages) {
+ if (!Flags.enableAdServiceFw()) {
+ return;
+ }
+ UserState userState = getOrCreateUserStateLocked(userId);
+ userState.mPackageSet.clear();
+
+ if (DEBUG) {
+ Slogf.d(TAG, "buildTvAdServiceListLocked");
+ }
+ PackageManager pm = mContext.getPackageManager();
+ List<ResolveInfo> services = pm.queryIntentServicesAsUser(
+ new Intent(TvAdService.SERVICE_INTERFACE),
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
+ userId);
+ List<TvAdServiceInfo> serviceList = new ArrayList<>();
+
+ for (ResolveInfo ri : services) {
+ ServiceInfo si = ri.serviceInfo;
+ if (!android.Manifest.permission.BIND_TV_AD_SERVICE.equals(si.permission)) {
+ Slog.w(TAG, "Skipping TV AD service " + si.name
+ + ": it does not require the permission "
+ + android.Manifest.permission.BIND_TV_AD_SERVICE);
+ continue;
+ }
+
+ ComponentName component = new ComponentName(si.packageName, si.name);
+ try {
+ TvAdServiceInfo info = new TvAdServiceInfo(mContext, component);
+ serviceList.add(info);
+ } catch (Exception e) {
+ Slogf.e(TAG, "failed to load TV AD service " + si.name, e);
+ continue;
+ }
+ userState.mPackageSet.add(si.packageName);
+ }
+
+ // sort the service list by service id
+ Collections.sort(serviceList, Comparator.comparing(TvAdServiceInfo::getId));
+ Map<String, TvAdServiceState> adServiceMap = new HashMap<>();
+ for (TvAdServiceInfo info : serviceList) {
+ String serviceId = info.getId();
+ if (DEBUG) {
+ Slogf.d(TAG, "add " + serviceId);
+ }
+ TvAdServiceState adServiceState = userState.mAdServiceMap.get(serviceId);
+ if (adServiceState == null) {
+ adServiceState = new TvAdServiceState();
+ }
+ adServiceState.mInfo = info;
+ adServiceState.mUid = getAdServiceUid(info);
+ adServiceState.mComponentName = info.getComponent();
+ adServiceMap.put(serviceId, adServiceState);
+ }
+
+ for (String serviceId : adServiceMap.keySet()) {
+ if (!userState.mAdServiceMap.containsKey(serviceId)) {
+ notifyAdServiceAddedLocked(userState, serviceId);
+ } else if (updatedPackages != null) {
+ // Notify the package updates
+ ComponentName component = adServiceMap.get(serviceId).mInfo.getComponent();
+ for (String updatedPackage : updatedPackages) {
+ if (component.getPackageName().equals(updatedPackage)) {
+ updateAdServiceConnectionLocked(component, userId);
+ notifyAdServiceUpdatedLocked(userState, serviceId);
+ break;
+ }
+ }
+ }
+ }
+
+ for (String serviceId : userState.mAdServiceMap.keySet()) {
+ if (!adServiceMap.containsKey(serviceId)) {
+ TvAdServiceInfo info = userState.mAdServiceMap.get(serviceId).mInfo;
+ AdServiceState serviceState = userState.mAdServiceStateMap.get(info.getComponent());
+ if (serviceState != null) {
+ abortPendingCreateAdSessionRequestsLocked(serviceState, serviceId, userId);
+ }
+ notifyAdServiceRemovedLocked(userState, serviceId);
+ }
+ }
+
+ userState.mIAppMap.clear();
+ userState.mAdServiceMap = adServiceMap;
+ }
+
+ @GuardedBy("mLock")
+ private void notifyAdServiceAddedLocked(UserState userState, String serviceId) {
+ if (DEBUG) {
+ Slog.d(TAG, "notifyAdServiceAddedLocked(serviceId=" + serviceId + ")");
+ }
+ int n = userState.mAdCallbacks.beginBroadcast();
+ for (int i = 0; i < n; ++i) {
+ try {
+ userState.mAdCallbacks.getBroadcastItem(i).onAdServiceAdded(serviceId);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "failed to report added AD service to callback", e);
+ }
+ }
+ userState.mAdCallbacks.finishBroadcast();
+ }
+
+ @GuardedBy("mLock")
+ private void notifyAdServiceRemovedLocked(UserState userState, String serviceId) {
+ if (DEBUG) {
+ Slog.d(TAG, "notifyAdServiceRemovedLocked(serviceId=" + serviceId + ")");
+ }
+ int n = userState.mAdCallbacks.beginBroadcast();
+ for (int i = 0; i < n; ++i) {
+ try {
+ userState.mAdCallbacks.getBroadcastItem(i).onAdServiceRemoved(serviceId);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "failed to report removed AD service to callback", e);
+ }
+ }
+ userState.mAdCallbacks.finishBroadcast();
+ }
+
+ @GuardedBy("mLock")
+ private void notifyAdServiceUpdatedLocked(UserState userState, String serviceId) {
+ if (DEBUG) {
+ Slog.d(TAG, "notifyAdServiceUpdatedLocked(serviceId=" + serviceId + ")");
+ }
+ int n = userState.mAdCallbacks.beginBroadcast();
+ for (int i = 0; i < n; ++i) {
+ try {
+ userState.mAdCallbacks.getBroadcastItem(i).onAdServiceUpdated(serviceId);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "failed to report updated AD service to callback", e);
+ }
+ }
+ userState.mAdCallbacks.finishBroadcast();
+ }
+
+ @GuardedBy("mLock")
private void notifyInteractiveAppServiceAddedLocked(UserState userState, String iAppServiceId) {
if (DEBUG) {
Slog.d(TAG, "notifyInteractiveAppServiceAddedLocked(iAppServiceId="
@@ -340,6 +486,16 @@
}
}
+ private int getAdServiceUid(TvAdServiceInfo info) {
+ try {
+ return getContext().getPackageManager().getApplicationInfo(
+ info.getServiceInfo().packageName, 0).uid;
+ } catch (PackageManager.NameNotFoundException e) {
+ Slogf.w(TAG, "Unable to get UID for " + info, e);
+ return Process.INVALID_UID;
+ }
+ }
+
@Override
public void onStart() {
if (DEBUG) {
@@ -357,6 +513,7 @@
synchronized (mLock) {
buildTvInteractiveAppServiceListLocked(mCurrentUserId, null);
buildAppLinkInfoLocked(mCurrentUserId);
+ buildTvAdServiceListLocked(mCurrentUserId, null);
}
}
}
@@ -372,6 +529,14 @@
}
}
}
+ private void buildTvAdServiceList(String[] packages) {
+ int userId = getChangingUserId();
+ synchronized (mLock) {
+ if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
+ buildTvAdServiceListLocked(userId, packages);
+ }
+ }
+ }
@Override
public void onPackageUpdateFinished(String packageName, int uid) {
@@ -379,6 +544,7 @@
// This callback is invoked when the TV interactive App service is reinstalled.
// In this case, isReplacing() always returns true.
buildTvInteractiveAppServiceList(new String[] { packageName });
+ buildTvAdServiceList(new String[] { packageName });
}
@Override
@@ -390,6 +556,7 @@
// available.
if (isReplacing()) {
buildTvInteractiveAppServiceList(packages);
+ buildTvAdServiceList(packages);
}
}
@@ -403,6 +570,7 @@
}
if (isReplacing()) {
buildTvInteractiveAppServiceList(packages);
+ buildTvAdServiceList(packages);
}
}
@@ -418,6 +586,7 @@
return;
}
buildTvInteractiveAppServiceList(null);
+ buildTvAdServiceList(null);
}
@Override
@@ -476,6 +645,7 @@
mCurrentUserId = userId;
buildTvInteractiveAppServiceListLocked(userId, null);
buildAppLinkInfoLocked(userId);
+ buildTvAdServiceListLocked(userId, null);
}
}
@@ -562,6 +732,7 @@
mRunningProfiles.add(userId);
buildTvInteractiveAppServiceListLocked(userId, null);
buildAppLinkInfoLocked(userId);
+ buildTvAdServiceListLocked(userId, null);
}
@GuardedBy("mLock")
@@ -619,7 +790,19 @@
Slog.e(TAG, "error in onSessionReleased", e);
}
}
- removeSessionStateLocked(state.mSessionToken, state.mUserId);
+ removeAdSessionStateLocked(state.mSessionToken, state.mUserId);
+ }
+
+ @GuardedBy("mLock")
+ private void clearAdSessionAndNotifyClientLocked(AdSessionState state) {
+ if (state.mClient != null) {
+ try {
+ state.mClient.onSessionReleased(state.mSeq);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in onSessionReleased", e);
+ }
+ }
+ removeAdSessionStateLocked(state.mSessionToken, state.mUserId);
}
private int resolveCallingUserId(int callingPid, int callingUid, int requestedUserId,
@@ -655,6 +838,44 @@
}
@GuardedBy("mLock")
+ private AdSessionState getAdSessionStateLocked(
+ IBinder sessionToken, int callingUid, int userId) {
+ UserState userState = getOrCreateUserStateLocked(userId);
+ return getAdSessionStateLocked(sessionToken, callingUid, userState);
+ }
+
+ @GuardedBy("mLock")
+ private AdSessionState getAdSessionStateLocked(IBinder sessionToken, int callingUid,
+ UserState userState) {
+ AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken);
+ if (sessionState == null) {
+ throw new SessionNotFoundException("Session state not found for token " + sessionToken);
+ }
+ // Only the application that requested this session or the system can access it.
+ if (callingUid != Process.SYSTEM_UID && callingUid != sessionState.mCallingUid) {
+ throw new SecurityException("Illegal access to the session with token " + sessionToken
+ + " from uid " + callingUid);
+ }
+ return sessionState;
+ }
+
+ @GuardedBy("mLock")
+ private ITvAdSession getAdSessionLocked(
+ IBinder sessionToken, int callingUid, int userId) {
+ return getAdSessionLocked(getAdSessionStateLocked(sessionToken, callingUid, userId));
+ }
+
+ @GuardedBy("mLock")
+ private ITvAdSession getAdSessionLocked(AdSessionState sessionState) {
+ ITvAdSession session = sessionState.mSession;
+ if (session == null) {
+ throw new IllegalStateException("Session not yet created for token "
+ + sessionState.mSessionToken);
+ }
+ return session;
+ }
+
+ @GuardedBy("mLock")
private SessionState getSessionStateLocked(IBinder sessionToken, int callingUid, int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
return getSessionStateLocked(sessionToken, callingUid, userState);
@@ -691,10 +912,200 @@
return session;
}
private final class TvAdBinderService extends ITvAdManager.Stub {
+
+ @Override
+ public List<TvAdServiceInfo> getTvAdServiceList(int userId) {
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, "getTvAdServiceList");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ if (!mGetAdServiceListCalled) {
+ buildTvAdServiceListLocked(userId, null);
+ mGetAdServiceListCalled = true;
+ }
+ UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+ List<TvAdServiceInfo> adServiceList = new ArrayList<>();
+ for (TvAdServiceState state : userState.mAdServiceMap.values()) {
+ adServiceList.add(state.mInfo);
+ }
+ return adServiceList;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void createSession(final ITvAdClient client, final String serviceId, String type,
+ int seq, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+ final int resolvedUserId = resolveCallingUserId(callingPid, callingUid,
+ userId, "createSession");
+ final long identity = Binder.clearCallingIdentity();
+
+ try {
+ synchronized (mLock) {
+ if (userId != mCurrentUserId && !mRunningProfiles.contains(userId)) {
+ // Only current user and its running profiles can create sessions.
+ // Let the client get onConnectionFailed callback for this case.
+ sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq);
+ return;
+ }
+ UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+ TvAdServiceState adState = userState.mAdMap.get(serviceId);
+ if (adState == null) {
+ Slogf.w(TAG, "Failed to find state for serviceId=" + serviceId);
+ sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq);
+ return;
+ }
+ AdServiceState serviceState =
+ userState.mAdServiceStateMap.get(adState.mComponentName);
+ if (serviceState == null) {
+ int tasUid = PackageManager.getApplicationInfoAsUserCached(
+ adState.mComponentName.getPackageName(), 0, resolvedUserId).uid;
+ serviceState = new AdServiceState(
+ adState.mComponentName, serviceId, resolvedUserId);
+ userState.mAdServiceStateMap.put(adState.mComponentName, serviceState);
+ }
+ // Send a null token immediately while reconnecting.
+ if (serviceState.mReconnecting) {
+ sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq);
+ return;
+ }
+
+ // Create a new session token and a session state.
+ IBinder sessionToken = new Binder();
+ AdSessionState sessionState = new AdSessionState(sessionToken, serviceId, type,
+ adState.mComponentName, client, seq, callingUid,
+ callingPid, resolvedUserId);
+
+ // Add them to the global session state map of the current user.
+ userState.mAdSessionStateMap.put(sessionToken, sessionState);
+
+ // Also, add them to the session state map of the current service.
+ serviceState.mSessionTokens.add(sessionToken);
+
+ if (serviceState.mService != null) {
+ if (!createAdSessionInternalLocked(serviceState.mService, sessionToken,
+ resolvedUserId)) {
+ removeAdSessionStateLocked(sessionToken, resolvedUserId);
+ }
+ } else {
+ updateAdServiceConnectionLocked(adState.mComponentName, resolvedUserId);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void releaseSession(IBinder sessionToken, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "releaseSession(sessionToken=" + sessionToken + ")");
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "releaseSession");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ releaseSessionLocked(sessionToken, callingUid, resolvedUserId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void setSurface(IBinder sessionToken, Surface surface, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "setSurface");
+ AdSessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getAdSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getAdSessionLocked(sessionState).setSurface(surface);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in setSurface", e);
+ }
+ }
+ } finally {
+ if (surface != null) {
+ // surface is not used in TvInteractiveAppManagerService.
+ surface.release();
+ }
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void dispatchSurfaceChanged(IBinder sessionToken, int format, int width,
+ int height, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "dispatchSurfaceChanged");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ AdSessionState sessionState = getAdSessionStateLocked(
+ sessionToken, callingUid, resolvedUserId);
+ getAdSessionLocked(sessionState).dispatchSurfaceChanged(format, width,
+ height);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in dispatchSurfaceChanged", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
@Override
public void startAdService(IBinder sessionToken, int userId) {
}
+ @Override
+ public void registerCallback(final ITvAdManagerCallback callback, int userId) {
+ int callingPid = Binder.getCallingPid();
+ int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
+ "registerCallback");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ final UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+ if (!userState.mAdCallbacks.register(callback)) {
+ Slog.e(TAG, "client process has already died");
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void unregisterCallback(ITvAdManagerCallback callback, int userId) {
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, "unregisterCallback");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ UserState userState = getOrCreateUserStateLocked(resolvedUserId);
+ userState.mAdCallbacks.unregister(callback);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
}
private final class BinderService extends ITvInteractiveAppManager.Stub {
@@ -927,7 +1338,7 @@
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- releaseSessionLocked(sessionToken, callingUid, resolvedUserId);
+ releaseAdSessionLocked(sessionToken, callingUid, resolvedUserId);
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -1471,6 +1882,32 @@
}
@Override
+ public void sendSelectedTrackInfo(IBinder sessionToken, List<TvTrackInfo> tracks,
+ int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendSelectedTrackInfo(tracks=%s)", tracks.toString());
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendSelectedTrackInfo");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).sendSelectedTrackInfo(tracks);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendSelectedTrackInfo", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void sendCurrentTvInputId(IBinder sessionToken, String inputId, int userId) {
if (DEBUG) {
Slogf.d(TAG, "sendCurrentTvInputId(inputId=%s)", inputId);
@@ -2134,6 +2571,17 @@
}
@GuardedBy("mLock")
+ private void sendAdSessionTokenToClientLocked(
+ ITvAdClient client, String serviceId, IBinder sessionToken,
+ InputChannel channel, int seq) {
+ try {
+ client.onSessionCreated(serviceId, sessionToken, channel, seq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onSessionCreated", e);
+ }
+ }
+
+ @GuardedBy("mLock")
private boolean createSessionInternalLocked(
ITvInteractiveAppService service, IBinder sessionToken, int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
@@ -2163,6 +2611,58 @@
}
@GuardedBy("mLock")
+ private boolean createAdSessionInternalLocked(
+ ITvAdService service, IBinder sessionToken, int userId) {
+ UserState userState = getOrCreateUserStateLocked(userId);
+ AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken);
+ if (DEBUG) {
+ Slogf.d(TAG, "createAdSessionInternalLocked(iAppServiceId="
+ + sessionState.mAdServiceId + ")");
+ }
+ InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString());
+
+ // Set up a callback to send the session token.
+ ITvAdSessionCallback callback = new AdSessionCallback(sessionState, channels);
+
+ boolean created = true;
+ // Create a session. When failed, send a null token immediately.
+ try {
+ service.createSession(
+ channels[1], callback, sessionState.mAdServiceId, sessionState.mType);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in createSession", e);
+ sendAdSessionTokenToClientLocked(sessionState.mClient, sessionState.mAdServiceId, null,
+ null, sessionState.mSeq);
+ created = false;
+ }
+ channels[1].dispose();
+ return created;
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ private AdSessionState releaseAdSessionLocked(
+ IBinder sessionToken, int callingUid, int userId) {
+ AdSessionState sessionState = null;
+ try {
+ sessionState = getAdSessionStateLocked(sessionToken, callingUid, userId);
+ UserState userState = getOrCreateUserStateLocked(userId);
+ if (sessionState.mSession != null) {
+ sessionState.mSession.asBinder().unlinkToDeath(sessionState, 0);
+ sessionState.mSession.release();
+ }
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in releaseSession", e);
+ } finally {
+ if (sessionState != null) {
+ sessionState.mSession = null;
+ }
+ }
+ removeAdSessionStateLocked(sessionToken, userId);
+ return sessionState;
+ }
+
+ @GuardedBy("mLock")
@Nullable
private SessionState releaseSessionLocked(IBinder sessionToken, int callingUid, int userId) {
SessionState sessionState = null;
@@ -2215,6 +2715,36 @@
}
@GuardedBy("mLock")
+ private void removeAdSessionStateLocked(IBinder sessionToken, int userId) {
+ UserState userState = getOrCreateUserStateLocked(userId);
+
+ // Remove the session state from the global session state map of the current user.
+ AdSessionState sessionState = userState.mAdSessionStateMap.remove(sessionToken);
+
+ if (sessionState == null) {
+ Slogf.e(TAG, "sessionState null, no more remove session action!");
+ return;
+ }
+
+ // Also remove the session token from the session token list of the current client and
+ // service.
+ ClientState clientState = userState.mClientStateMap.get(sessionState.mClient.asBinder());
+ if (clientState != null) {
+ clientState.mSessionTokens.remove(sessionToken);
+ if (clientState.isEmpty()) {
+ userState.mClientStateMap.remove(sessionState.mClient.asBinder());
+ sessionState.mClient.asBinder().unlinkToDeath(clientState, 0);
+ }
+ }
+
+ AdServiceState serviceState = userState.mAdServiceStateMap.get(sessionState.mComponent);
+ if (serviceState != null) {
+ serviceState.mSessionTokens.remove(sessionToken);
+ }
+ updateAdServiceConnectionLocked(sessionState.mComponent, userId);
+ }
+
+ @GuardedBy("mLock")
private void abortPendingCreateSessionRequestsLocked(ServiceState serviceState,
String iAppServiceId, int userId) {
// Let clients know the create session requests are failed.
@@ -2237,6 +2767,28 @@
}
@GuardedBy("mLock")
+ private void abortPendingCreateAdSessionRequestsLocked(AdServiceState serviceState,
+ String serviceId, int userId) {
+ // Let clients know the create session requests are failed.
+ UserState userState = getOrCreateUserStateLocked(userId);
+ List<AdSessionState> sessionsToAbort = new ArrayList<>();
+ for (IBinder sessionToken : serviceState.mSessionTokens) {
+ AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken);
+ if (sessionState.mSession == null
+ && (serviceState == null
+ || sessionState.mAdServiceId.equals(serviceId))) {
+ sessionsToAbort.add(sessionState);
+ }
+ }
+ for (AdSessionState sessionState : sessionsToAbort) {
+ removeAdSessionStateLocked(sessionState.mSessionToken, sessionState.mUserId);
+ sendAdSessionTokenToClientLocked(sessionState.mClient,
+ sessionState.mAdServiceId, null, null, sessionState.mSeq);
+ }
+ updateAdServiceConnectionLocked(serviceState.mComponent, userId);
+ }
+
+ @GuardedBy("mLock")
private void updateServiceConnectionLocked(ComponentName component, int userId) {
UserState userState = getOrCreateUserStateLocked(userId);
ServiceState serviceState = userState.mServiceStateMap.get(component);
@@ -2284,10 +2836,64 @@
}
}
+ @GuardedBy("mLock")
+ private void updateAdServiceConnectionLocked(ComponentName component, int userId) {
+ UserState userState = getOrCreateUserStateLocked(userId);
+ AdServiceState serviceState = userState.mAdServiceStateMap.get(component);
+ if (serviceState == null) {
+ return;
+ }
+ if (serviceState.mReconnecting) {
+ if (!serviceState.mSessionTokens.isEmpty()) {
+ // wait until all the sessions are removed.
+ return;
+ }
+ serviceState.mReconnecting = false;
+ }
+
+ boolean shouldBind = (!serviceState.mSessionTokens.isEmpty())
+ || (!serviceState.mPendingAppLinkCommand.isEmpty());
+
+ if (serviceState.mService == null && shouldBind) {
+ // This means that the service is not yet connected but its state indicates that we
+ // have pending requests. Then, connect the service.
+ if (serviceState.mBound) {
+ // We have already bound to the service so we don't try to bind again until after we
+ // unbind later on.
+ return;
+ }
+ if (DEBUG) {
+ Slogf.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")");
+ }
+
+ Intent i = new Intent(TvAdService.SERVICE_INTERFACE).setComponent(component);
+ serviceState.mBound = mContext.bindServiceAsUser(
+ i, serviceState.mConnection,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
+ new UserHandle(userId));
+ } else if (serviceState.mService != null && !shouldBind) {
+ // This means that the service is already connected but its state indicates that we have
+ // nothing to do with it. Then, disconnect the service.
+ if (DEBUG) {
+ Slogf.d(TAG, "unbindService(service=" + component + ")");
+ }
+ mContext.unbindService(serviceState.mConnection);
+ userState.mAdServiceStateMap.remove(component);
+ }
+ }
+
private static final class UserState {
private final int mUserId;
+ // A mapping from the TV AD service ID to its TvAdServiceState.
+ private Map<String, TvAdServiceState> mAdMap = new HashMap<>();
+ // A mapping from the name of a TV Interactive App service to its state.
+ private final Map<ComponentName, AdServiceState> mAdServiceStateMap = new HashMap<>();
+ // A mapping from the token of a TV Interactive App session to its state.
+ private final Map<IBinder, AdSessionState> mAdSessionStateMap = new HashMap<>();
// A mapping from the TV Interactive App ID to its TvInteractiveAppState.
private Map<String, TvInteractiveAppState> mIAppMap = new HashMap<>();
+ // A mapping from the TV AD service ID to its TvAdServiceState.
+ private Map<String, TvAdServiceState> mAdServiceMap = new HashMap<>();
// A mapping from the token of a client to its state.
private final Map<IBinder, ClientState> mClientStateMap = new HashMap<>();
// A mapping from the name of a TV Interactive App service to its state.
@@ -2299,6 +2905,8 @@
private final Set<String> mPackageSet = new HashSet<>();
// A list of all app link infos.
private final List<AppLinkInfo> mAppLinkInfoList = new ArrayList<>();
+ private final RemoteCallbackList<ITvAdManagerCallback> mAdCallbacks =
+ new RemoteCallbackList<>();
// A list of callbacks.
private final RemoteCallbackList<ITvInteractiveAppManagerCallback> mCallbacks =
@@ -2317,7 +2925,16 @@
private int mIAppNumber;
}
+ private static final class TvAdServiceState {
+ private String mAdServiceId;
+ private ComponentName mComponentName;
+ private TvAdServiceInfo mInfo;
+ private int mUid;
+ private int mAdNumber;
+ }
+
private final class SessionState implements IBinder.DeathRecipient {
+ // TODO: rename SessionState and reorganize classes / methods of this file
private final IBinder mSessionToken;
private ITvInteractiveAppSession mSession;
private final String mIAppServiceId;
@@ -2359,6 +2976,49 @@
}
}
+ private final class AdSessionState implements IBinder.DeathRecipient {
+ private final IBinder mSessionToken;
+ private ITvAdSession mSession;
+ private final String mAdServiceId;
+
+ private final String mType;
+ private final ITvAdClient mClient;
+ private final int mSeq;
+ private final ComponentName mComponent;
+
+ // The UID of the application that created the session.
+ // The application is usually the TV app.
+ private final int mCallingUid;
+
+ // The PID of the application that created the session.
+ // The application is usually the TV app.
+ private final int mCallingPid;
+
+ private final int mUserId;
+
+ private AdSessionState(IBinder sessionToken, String serviceId, String type,
+ ComponentName componentName, ITvAdClient client, int seq,
+ int callingUid, int callingPid, int userId) {
+ mSessionToken = sessionToken;
+ mAdServiceId = serviceId;
+ mType = type;
+ mComponent = componentName;
+ mClient = client;
+ mSeq = seq;
+ mCallingUid = callingUid;
+ mCallingPid = callingPid;
+ mUserId = userId;
+ }
+
+ @Override
+ public void binderDied() {
+ synchronized (mLock) {
+ mSession = null;
+ clearAdSessionAndNotifyClientLocked(this);
+ }
+ }
+ }
+
private final class ClientState implements IBinder.DeathRecipient {
private final List<IBinder> mSessionTokens = new ArrayList<>();
@@ -2429,6 +3089,29 @@
}
}
+ private final class AdServiceState {
+ private final List<IBinder> mSessionTokens = new ArrayList<>();
+ private final ServiceConnection mConnection;
+ private final ComponentName mComponent;
+ private final String mAdServiceId;
+ private final List<Bundle> mPendingAppLinkCommand = new ArrayList<>();
+
+ private ITvAdService mService;
+ private AdServiceCallback mCallback;
+ private boolean mBound;
+ private boolean mReconnecting;
+
+ private AdServiceState(ComponentName component, String tasId, int userId) {
+ mComponent = component;
+ mConnection = new AdServiceConnection(component, userId);
+ mAdServiceId = tasId;
+ }
+
+ private void addPendingAppLinkCommand(Bundle command) {
+ mPendingAppLinkCommand.add(command);
+ }
+ }
+
private final class InteractiveAppServiceConnection implements ServiceConnection {
private final ComponentName mComponent;
private final int mUserId;
@@ -2542,6 +3225,98 @@
}
}
+ private final class AdServiceConnection implements ServiceConnection {
+ private final ComponentName mComponent;
+ private final int mUserId;
+
+ private AdServiceConnection(ComponentName component, int userId) {
+ mComponent = component;
+ mUserId = userId;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName component, IBinder service) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onServiceConnected(component=" + component + ")");
+ }
+ synchronized (mLock) {
+ UserState userState = getUserStateLocked(mUserId);
+ if (userState == null) {
+ // The user was removed while connecting.
+ mContext.unbindService(this);
+ return;
+ }
+ AdServiceState serviceState = userState.mAdServiceStateMap.get(mComponent);
+ serviceState.mService = ITvAdService.Stub.asInterface(service);
+
+ // Register a callback, if we need to.
+ if (serviceState.mCallback == null) {
+ serviceState.mCallback = new AdServiceCallback(mComponent, mUserId);
+ try {
+ serviceState.mService.registerCallback(serviceState.mCallback);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in registerCallback", e);
+ }
+ }
+
+ if (!serviceState.mPendingAppLinkCommand.isEmpty()) {
+ for (Iterator<Bundle> it = serviceState.mPendingAppLinkCommand.iterator();
+ it.hasNext(); ) {
+ Bundle command = it.next();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ serviceState.mService.sendAppLinkCommand(command);
+ it.remove();
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in sendAppLinkCommand(" + command
+ + ") when onServiceConnected", e);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ List<IBinder> tokensToBeRemoved = new ArrayList<>();
+
+ // And create sessions, if any.
+ for (IBinder sessionToken : serviceState.mSessionTokens) {
+ if (!createAdSessionInternalLocked(
+ serviceState.mService, sessionToken, mUserId)) {
+ tokensToBeRemoved.add(sessionToken);
+ }
+ }
+
+ for (IBinder sessionToken : tokensToBeRemoved) {
+ removeAdSessionStateLocked(sessionToken, mUserId);
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName component) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onServiceDisconnected(component=" + component + ")");
+ }
+ if (!mComponent.equals(component)) {
+ throw new IllegalArgumentException("Mismatched ComponentName: "
+ + mComponent + " (expected), " + component + " (actual).");
+ }
+ synchronized (mLock) {
+ UserState userState = getOrCreateUserStateLocked(mUserId);
+ AdServiceState serviceState = userState.mAdServiceStateMap.get(mComponent);
+ if (serviceState != null) {
+ serviceState.mReconnecting = true;
+ serviceState.mBound = false;
+ serviceState.mService = null;
+ serviceState.mCallback = null;
+
+ abortPendingCreateAdSessionRequestsLocked(serviceState, null, mUserId);
+ }
+ }
+ }
+ }
+
+
private final class ServiceCallback extends ITvInteractiveAppServiceCallback.Stub {
private final ComponentName mComponent;
private final int mUserId;
@@ -2567,6 +3342,17 @@
}
}
+
+ private final class AdServiceCallback extends ITvAdServiceCallback.Stub {
+ private final ComponentName mComponent;
+ private final int mUserId;
+
+ AdServiceCallback(ComponentName component, int userId) {
+ mComponent = component;
+ mUserId = userId;
+ }
+ }
+
private final class SessionCallback extends ITvInteractiveAppSessionCallback.Stub {
private final SessionState mSessionState;
private final InputChannel[] mInputChannels;
@@ -2798,6 +3584,23 @@
}
@Override
+ public void onRequestSelectedTrackInfo() {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestSelectedTrackInfo");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestSelectedTrackInfo(mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestSelectedTrackInfo", e);
+ }
+ }
+ }
+
+ @Override
public void onRequestCurrentTvInputId() {
synchronized (mLock) {
if (DEBUG) {
@@ -3110,6 +3913,85 @@
}
}
+ private final class AdSessionCallback extends ITvAdSessionCallback.Stub {
+ private final AdSessionState mSessionState;
+ private final InputChannel[] mInputChannels;
+
+ AdSessionCallback(AdSessionState sessionState, InputChannel[] channels) {
+ mSessionState = sessionState;
+ mInputChannels = channels;
+ }
+
+ @Override
+ public void onSessionCreated(ITvAdSession session) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onSessionCreated(adServiceId="
+ + mSessionState.mAdServiceId + ")");
+ }
+ synchronized (mLock) {
+ mSessionState.mSession = session;
+ if (session != null && addAdSessionTokenToClientStateLocked(session)) {
+ sendAdSessionTokenToClientLocked(
+ mSessionState.mClient,
+ mSessionState.mAdServiceId,
+ mSessionState.mSessionToken,
+ mInputChannels[0],
+ mSessionState.mSeq);
+ } else {
+ removeAdSessionStateLocked(mSessionState.mSessionToken, mSessionState.mUserId);
+ sendAdSessionTokenToClientLocked(mSessionState.mClient,
+ mSessionState.mAdServiceId, null, null, mSessionState.mSeq);
+ }
+ mInputChannels[0].dispose();
+ }
+ }
+
+ @Override
+ public void onLayoutSurface(int left, int top, int right, int bottom) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top
+ + ", right=" + right + ", bottom=" + bottom + ",)");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onLayoutSurface(left, top, right, bottom,
+ mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onLayoutSurface", e);
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ private boolean addAdSessionTokenToClientStateLocked(ITvAdSession session) {
+ try {
+ session.asBinder().linkToDeath(mSessionState, 0);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "session process has already died", e);
+ return false;
+ }
+
+ IBinder clientToken = mSessionState.mClient.asBinder();
+ UserState userState = getOrCreateUserStateLocked(mSessionState.mUserId);
+ ClientState clientState = userState.mClientStateMap.get(clientToken);
+ if (clientState == null) {
+ clientState = new ClientState(clientToken, mSessionState.mUserId);
+ try {
+ clientToken.linkToDeath(clientState, 0);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "client process has already died", e);
+ return false;
+ }
+ userState.mClientStateMap.put(clientToken, clientState);
+ }
+ clientState.mSessionTokens.add(mSessionState.mSessionToken);
+ return true;
+ }
+ }
+
private static class SessionNotFoundException extends IllegalArgumentException {
SessionNotFoundException(String name) {
super(name);
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
index e54b40e..03c75e0 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
@@ -37,7 +37,17 @@
void revokeUriPermission(String targetPackage, int callingUid,
GrantUri grantUri, final int modeFlags);
- boolean checkUriPermission(GrantUri grantUri, int uid, final int modeFlags);
+ /**
+ * Check if the uid has permission to the URI in grantUri.
+ *
+ * @param isFullAccessForContentUri If true, the URI has to be a content URI
+ * and the method will consider full access.
+ * Otherwise, the method will only consider
+ * URI grants.
+ */
+ boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags,
+ boolean isFullAccessForContentUri);
+
int checkGrantUriPermission(
int callingUid, String targetPkg, Uri uri, int modeFlags, int userId);
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
index e501b9d..ce2cbed 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
@@ -25,6 +25,7 @@
import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
@@ -1103,7 +1104,8 @@
*/
private int checkGrantUriPermissionUnlocked(int callingUid, String targetPkg, GrantUri grantUri,
int modeFlags, int lastTargetUid) {
- if (!Intent.isAccessUriMode(modeFlags)) {
+ if (!isContentUriWithAccessModeFlags(grantUri, modeFlags,
+ /* logAction */ "grant URI permission")) {
return -1;
}
@@ -1111,12 +1113,6 @@
if (DEBUG) Slog.v(TAG, "Checking grant " + targetPkg + " permission to " + grantUri);
}
- // If this is not a content: uri, we can't do anything with it.
- if (!ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) {
- if (DEBUG) Slog.v(TAG, "Can't grant URI permission for non-content URI: " + grantUri);
- return -1;
- }
-
// Bail early if system is trying to hand out permissions directly; it
// must always grant permissions on behalf of someone explicit.
final int callingAppId = UserHandle.getAppId(callingUid);
@@ -1137,7 +1133,7 @@
final String authority = grantUri.uri.getAuthority();
final ProviderInfo pi = getProviderInfo(authority, grantUri.sourceUserId,
- MATCH_DEBUG_TRIAGED_MISSING, callingUid);
+ MATCH_DIRECT_BOOT_AUTO, callingUid);
if (pi == null) {
Slog.w(TAG, "No content provider found for permission check: " +
grantUri.uri.toSafeString());
@@ -1285,6 +1281,65 @@
return targetUid;
}
+ private boolean isContentUriWithAccessModeFlags(GrantUri grantUri, int modeFlags,
+ String logAction) {
+ if (!Intent.isAccessUriMode(modeFlags)) {
+ if (DEBUG) Slog.v(TAG, "Mode flags are not access URI mode flags: " + modeFlags);
+ return false;
+ }
+
+ if (!ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) {
+ if (DEBUG) {
+ Slog.v(TAG, "Can't " + logAction + " on non-content URI: " + grantUri);
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /** Check if the uid has permission to the content URI in grantUri. */
+ private boolean checkContentUriPermissionFullUnlocked(GrantUri grantUri, int uid,
+ int modeFlags) {
+ if (uid < 0) {
+ throw new IllegalArgumentException("Uid must be positive for the content URI "
+ + "permission check of " + grantUri.uri.toSafeString());
+ }
+
+ if (!isContentUriWithAccessModeFlags(grantUri, modeFlags,
+ /* logAction */ "check content URI permission")) {
+ throw new IllegalArgumentException("The URI must be a content URI and the mode "
+ + "flags must be at least read and/or write for the content URI permission "
+ + "check of " + grantUri.uri.toSafeString());
+ }
+
+ final int appId = UserHandle.getAppId(uid);
+ if ((appId == SYSTEM_UID) || (appId == ROOT_UID)) {
+ return true;
+ }
+
+ // Retrieve the URI's content provider
+ final String authority = grantUri.uri.getAuthority();
+ ProviderInfo pi = getProviderInfo(authority, grantUri.sourceUserId, MATCH_DIRECT_BOOT_AUTO,
+ uid);
+
+ if (pi == null) {
+ Slog.w(TAG, "No content provider found for content URI permission check: "
+ + grantUri.uri.toSafeString());
+ return false;
+ }
+
+ // Check if it has general permission to the URI's content provider
+ if (checkHoldingPermissionsUnlocked(pi, grantUri, uid, modeFlags)) {
+ return true;
+ }
+
+ // Check if it has explicitly granted permissions to the URI
+ synchronized (mLock) {
+ return checkUriPermissionLocked(grantUri, uid, modeFlags);
+ }
+ }
+
/**
* @param userId The userId in which the uri is to be resolved.
*/
@@ -1482,7 +1537,12 @@
}
@Override
- public boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags) {
+ public boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags,
+ boolean isFullAccessForContentUri) {
+ if (isFullAccessForContentUri) {
+ return UriGrantsManagerService.this.checkContentUriPermissionFullUnlocked(grantUri,
+ uid, modeFlags);
+ }
synchronized (mLock) {
return UriGrantsManagerService.this.checkUriPermissionLocked(grantUri, uid,
modeFlags);
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
index 29782d9..f4fb1a1 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
@@ -159,11 +159,28 @@
}
}
+ private boolean shouldTriggerRepairLocked() {
+ if (mCurrentWebViewPackage == null) {
+ return true;
+ }
+ WebViewProviderInfo defaultProvider = getDefaultWebViewPackage();
+ if (mCurrentWebViewPackage.packageName.equals(defaultProvider.packageName)) {
+ List<UserPackage> userPackages =
+ mSystemInterface.getPackageInfoForProviderAllUsers(
+ mContext, defaultProvider);
+ return !isInstalledAndEnabledForAllUsers(userPackages);
+ } else {
+ return false;
+ }
+ }
+
@Override
public void prepareWebViewInSystemServer() {
try {
+ boolean repairNeeded = true;
synchronized (mLock) {
mCurrentWebViewPackage = findPreferredWebViewPackage();
+ repairNeeded = shouldTriggerRepairLocked();
String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext);
if (userSetting != null
&& !userSetting.equals(mCurrentWebViewPackage.packageName)) {
@@ -177,26 +194,25 @@
}
onWebViewProviderChanged(mCurrentWebViewPackage);
}
+
+ if (repairNeeded) {
+ // We didn't find a valid WebView implementation. Try explicitly re-enabling the
+ // default package for all users in case it was disabled, even if we already did the
+ // one-time migration before. If this actually changes the state, we will see the
+ // PackageManager broadcast shortly and try again.
+ WebViewProviderInfo defaultProvider = getDefaultWebViewPackage();
+ Slog.w(
+ TAG,
+ "No provider available for all users, trying to enable "
+ + defaultProvider.packageName);
+ mSystemInterface.enablePackageForAllUsers(
+ mContext, defaultProvider.packageName, true);
+ }
+
} catch (Throwable t) {
// Log and discard errors at this stage as we must not crash the system server.
Slog.e(TAG, "error preparing webview provider from system server", t);
}
-
- if (getCurrentWebViewPackage() == null) {
- // We didn't find a valid WebView implementation. Try explicitly re-enabling the
- // fallback package for all users in case it was disabled, even if we already did the
- // one-time migration before. If this actually changes the state, we will see the
- // PackageManager broadcast shortly and try again.
- WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages();
- WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
- if (fallbackProvider != null) {
- Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName);
- mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName,
- true);
- } else {
- Slog.e(TAG, "No valid provider and no fallback available.");
- }
- }
}
private void startZygoteWhenReady() {
@@ -421,42 +437,43 @@
/**
* Returns either the package info of the WebView provider determined in the following way:
- * If the user has chosen a provider then use that if it is valid,
- * otherwise use the first package in the webview priority list that is valid.
- *
+ * If the user has chosen a provider then use that if it is valid, enabled and installed
+ * for all users, otherwise use the default provider.
*/
private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException {
- ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos();
-
- String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext);
-
// If the user has chosen provider, use that (if it's installed and enabled for all
// users).
- for (ProviderAndPackageInfo providerAndPackage : providers) {
- if (providerAndPackage.provider.packageName.equals(userChosenProvider)) {
- // userPackages can contain null objects.
- List<UserPackage> userPackages =
- mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
- providerAndPackage.provider);
- if (isInstalledAndEnabledForAllUsers(userPackages)) {
- return providerAndPackage.packageInfo;
+ String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext);
+ WebViewProviderInfo userChosenProvider =
+ getWebViewProviderForPackage(userChosenPackageName);
+ if (userChosenProvider != null) {
+ try {
+ PackageInfo packageInfo =
+ mSystemInterface.getPackageInfoForProvider(userChosenProvider);
+ if (validityResult(userChosenProvider, packageInfo) == VALIDITY_OK) {
+ List<UserPackage> userPackages =
+ mSystemInterface.getPackageInfoForProviderAllUsers(
+ mContext, userChosenProvider);
+ if (isInstalledAndEnabledForAllUsers(userPackages)) {
+ return packageInfo;
+ }
}
+ } catch (NameNotFoundException e) {
+ Slog.w(TAG, "User chosen WebView package (" + userChosenPackageName
+ + ") not found");
}
}
- // User did not choose, or the choice failed; use the most stable provider that is
- // installed and enabled for all users, and available by default (not through
- // user choice).
- for (ProviderAndPackageInfo providerAndPackage : providers) {
- if (providerAndPackage.provider.availableByDefault) {
- // userPackages can contain null objects.
- List<UserPackage> userPackages =
- mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
- providerAndPackage.provider);
- if (isInstalledAndEnabledForAllUsers(userPackages)) {
- return providerAndPackage.packageInfo;
- }
+ // User did not choose, or the choice failed; return the default provider even if it is not
+ // installed or enabled for all users.
+ WebViewProviderInfo defaultProvider = getDefaultWebViewPackage();
+ try {
+ PackageInfo packageInfo = mSystemInterface.getPackageInfoForProvider(defaultProvider);
+ if (validityResult(defaultProvider, packageInfo) == VALIDITY_OK) {
+ return packageInfo;
}
+ } catch (NameNotFoundException e) {
+ Slog.w(TAG, "Default WebView package (" + defaultProvider.packageName + ") not found");
}
// This should never happen during normal operation (only with modified system images).
@@ -464,6 +481,16 @@
throw new WebViewPackageMissingException("Could not find a loadable WebView package");
}
+ private WebViewProviderInfo getWebViewProviderForPackage(String packageName) {
+ WebViewProviderInfo[] allProviders = getWebViewPackages();
+ for (int n = 0; n < allProviders.length; n++) {
+ if (allProviders[n].packageName.equals(packageName)) {
+ return allProviders[n];
+ }
+ }
+ return null;
+ }
+
/**
* Return true iff {@param packageInfos} point to only installed and enabled packages.
* The given packages {@param packageInfos} should all be pointing to the same package, but each
diff --git a/services/core/java/com/android/server/wm/LegacyTransitionTracer.java b/services/core/java/com/android/server/wm/LegacyTransitionTracer.java
new file mode 100644
index 0000000..fb2d5be
--- /dev/null
+++ b/services/core/java/com/android/server/wm/LegacyTransitionTracer.java
@@ -0,0 +1,331 @@
+/*
+ * 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.wm;
+
+import static android.os.Build.IS_USER;
+
+import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER;
+import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_H;
+import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_L;
+import static com.android.server.wm.shell.TransitionTraceProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.TraceBuffer;
+import com.android.server.wm.Transition.ChangeInfo;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper class to collect and dump transition traces.
+ */
+class LegacyTransitionTracer implements TransitionTracer {
+
+ private static final String LOG_TAG = "TransitionTracer";
+
+ private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB
+ private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB
+
+ // This will be the size the proto output streams are initialized to.
+ // Ideally this should fit most or all the proto objects we will create and be no bigger than
+ // that to ensure to don't use excessive amounts of memory.
+ private static final int CHUNK_SIZE = 64;
+
+ static final String WINSCOPE_EXT = ".winscope";
+ private static final String TRACE_FILE =
+ "/data/misc/wmtrace/wm_transition_trace" + WINSCOPE_EXT;
+ private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+
+ private final TraceBuffer mTraceBuffer = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY);
+
+ private final Object mEnabledLock = new Object();
+ private volatile boolean mActiveTracingEnabled = false;
+
+ /**
+ * Records key information about a transition that has been sent to Shell to be played.
+ * More information will be appended to the same proto object once the transition is finished or
+ * aborted.
+ * Transition information won't be added to the trace buffer until
+ * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this
+ * transition.
+ *
+ * @param transition The transition that has been sent to Shell.
+ * @param targets Information about the target windows of the transition.
+ */
+ @Override
+ public void logSentTransition(Transition transition, ArrayList<ChangeInfo> targets) {
+ try {
+ final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+ final long protoToken = outputStream
+ .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+ outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
+ outputStream.write(com.android.server.wm.shell.Transition.CREATE_TIME_NS,
+ transition.mLogger.mCreateTimeNs);
+ outputStream.write(com.android.server.wm.shell.Transition.SEND_TIME_NS,
+ transition.mLogger.mSendTimeNs);
+ outputStream.write(com.android.server.wm.shell.Transition.START_TRANSACTION_ID,
+ transition.getStartTransaction().getId());
+ outputStream.write(com.android.server.wm.shell.Transition.FINISH_TRANSACTION_ID,
+ transition.getFinishTransaction().getId());
+ dumpTransitionTargetsToProto(outputStream, transition, targets);
+ outputStream.end(protoToken);
+
+ mTraceBuffer.add(outputStream);
+ } catch (Exception e) {
+ // Don't let any errors in the tracing cause the transition to fail
+ Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+ }
+ }
+
+ /**
+ * Completes the information dumped in {@link #logSentTransition} for a transition
+ * that has finished or aborted, and add the proto object to the trace buffer.
+ *
+ * @param transition The transition that has finished.
+ */
+ @Override
+ public void logFinishedTransition(Transition transition) {
+ try {
+ final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+ final long protoToken = outputStream
+ .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+ outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
+ outputStream.write(com.android.server.wm.shell.Transition.FINISH_TIME_NS,
+ transition.mLogger.mFinishTimeNs);
+ outputStream.end(protoToken);
+
+ mTraceBuffer.add(outputStream);
+ } catch (Exception e) {
+ // Don't let any errors in the tracing cause the transition to fail
+ Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+ }
+ }
+
+ /**
+ * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer
+ * unless actively tracing.
+ *
+ * @param transition The transition that has been aborted
+ */
+ @Override
+ public void logAbortedTransition(Transition transition) {
+ try {
+ final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+ final long protoToken = outputStream
+ .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+ outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
+ outputStream.write(com.android.server.wm.shell.Transition.ABORT_TIME_NS,
+ transition.mLogger.mAbortTimeNs);
+ outputStream.end(protoToken);
+
+ mTraceBuffer.add(outputStream);
+ } catch (Exception e) {
+ // Don't let any errors in the tracing cause the transition to fail
+ Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+ }
+ }
+
+ @Override
+ public void logRemovingStartingWindow(@NonNull StartingData startingData) {
+ if (startingData.mTransitionId == 0) {
+ return;
+ }
+ try {
+ final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+ final long protoToken = outputStream
+ .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+ outputStream.write(com.android.server.wm.shell.Transition.ID,
+ startingData.mTransitionId);
+ outputStream.write(
+ com.android.server.wm.shell.Transition.STARTING_WINDOW_REMOVE_TIME_NS,
+ SystemClock.elapsedRealtimeNanos());
+ outputStream.end(protoToken);
+
+ mTraceBuffer.add(outputStream);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+ }
+ }
+
+ private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream,
+ Transition transition, ArrayList<ChangeInfo> targets) {
+ Trace.beginSection("TransitionTracer#dumpTransitionTargetsToProto");
+ if (mActiveTracingEnabled) {
+ outputStream.write(com.android.server.wm.shell.Transition.ID,
+ transition.getSyncId());
+ }
+
+ outputStream.write(com.android.server.wm.shell.Transition.TYPE, transition.mType);
+ outputStream.write(com.android.server.wm.shell.Transition.FLAGS, transition.getFlags());
+
+ for (int i = 0; i < targets.size(); ++i) {
+ final long changeToken = outputStream
+ .start(com.android.server.wm.shell.Transition.TARGETS);
+
+ final Transition.ChangeInfo target = targets.get(i);
+
+ final int layerId;
+ if (target.mContainer.mSurfaceControl.isValid()) {
+ layerId = target.mContainer.mSurfaceControl.getLayerId();
+ } else {
+ layerId = -1;
+ }
+
+ outputStream.write(com.android.server.wm.shell.Target.MODE, target.mReadyMode);
+ outputStream.write(com.android.server.wm.shell.Target.FLAGS, target.mReadyFlags);
+ outputStream.write(com.android.server.wm.shell.Target.LAYER_ID, layerId);
+
+ if (mActiveTracingEnabled) {
+ // What we use in the WM trace
+ final int windowId = System.identityHashCode(target.mContainer);
+ outputStream.write(com.android.server.wm.shell.Target.WINDOW_ID, windowId);
+ }
+
+ outputStream.end(changeToken);
+ }
+
+ Trace.endSection();
+ }
+
+ /**
+ * Starts collecting transitions for the trace.
+ * If called while a trace is already running, this will reset the trace.
+ */
+ @Override
+ public void startTrace(@Nullable PrintWriter pw) {
+ if (IS_USER) {
+ LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+ return;
+ }
+ Trace.beginSection("TransitionTracer#startTrace");
+ LogAndPrintln.i(pw, "Starting shell transition trace.");
+ synchronized (mEnabledLock) {
+ mActiveTracingEnabled = true;
+ mTraceBuffer.resetBuffer();
+ mTraceBuffer.setCapacity(ACTIVE_TRACING_BUFFER_CAPACITY);
+ }
+ Trace.endSection();
+ }
+
+ /**
+ * Stops collecting the transition trace and dump to trace to file.
+ *
+ * Dumps the trace to @link{TRACE_FILE}.
+ */
+ @Override
+ public void stopTrace(@Nullable PrintWriter pw) {
+ stopTrace(pw, new File(TRACE_FILE));
+ }
+
+ /**
+ * Stops collecting the transition trace and dump to trace to file.
+ * @param outputFile The file to dump the transition trace to.
+ */
+ public void stopTrace(@Nullable PrintWriter pw, File outputFile) {
+ if (IS_USER) {
+ LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+ return;
+ }
+ Trace.beginSection("TransitionTracer#stopTrace");
+ LogAndPrintln.i(pw, "Stopping shell transition trace.");
+ synchronized (mEnabledLock) {
+ mActiveTracingEnabled = false;
+ writeTraceToFileLocked(pw, outputFile);
+ mTraceBuffer.resetBuffer();
+ mTraceBuffer.setCapacity(ALWAYS_ON_TRACING_CAPACITY);
+ }
+ Trace.endSection();
+ }
+
+ /**
+ * Being called while taking a bugreport so that tracing files can be included in the bugreport.
+ *
+ * @param pw Print writer
+ */
+ @Override
+ public void saveForBugreport(@Nullable PrintWriter pw) {
+ if (IS_USER) {
+ LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+ return;
+ }
+ Trace.beginSection("TransitionTracer#saveForBugreport");
+ synchronized (mEnabledLock) {
+ final File outputFile = new File(TRACE_FILE);
+ writeTraceToFileLocked(pw, outputFile);
+ }
+ Trace.endSection();
+ }
+
+ @Override
+ public boolean isTracing() {
+ return mActiveTracingEnabled;
+ }
+
+ private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) {
+ Trace.beginSection("TransitionTracer#writeTraceToFileLocked");
+ try {
+ ProtoOutputStream proto = new ProtoOutputStream(CHUNK_SIZE);
+ proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
+ long timeOffsetNs =
+ TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
+ - SystemClock.elapsedRealtimeNanos();
+ proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
+ int pid = android.os.Process.myPid();
+ LogAndPrintln.i(pw, "Writing file to " + file.getAbsolutePath()
+ + " from process " + pid);
+ mTraceBuffer.writeTraceToFile(file, proto);
+ } catch (IOException e) {
+ LogAndPrintln.e(pw, "Unable to write buffer to file", e);
+ }
+ Trace.endSection();
+ }
+
+ private static class LogAndPrintln {
+ private static void i(@Nullable PrintWriter pw, String msg) {
+ Log.i(LOG_TAG, msg);
+ if (pw != null) {
+ pw.println(msg);
+ pw.flush();
+ }
+ }
+
+ private static void e(@Nullable PrintWriter pw, String msg) {
+ Log.e(LOG_TAG, msg);
+ if (pw != null) {
+ pw.println("ERROR: " + msg);
+ pw.flush();
+ }
+ }
+
+ private static void e(@Nullable PrintWriter pw, String msg, @NonNull Exception e) {
+ Log.e(LOG_TAG, msg, e);
+ if (pw != null) {
+ pw.println("ERROR: " + msg + " ::\n " + e);
+ pw.flush();
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
new file mode 100644
index 0000000..eae9951
--- /dev/null
+++ b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2023 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.wm;
+
+import android.annotation.NonNull;
+import android.internal.perfetto.protos.PerfettoTrace;
+import android.os.SystemClock;
+import android.tracing.perfetto.DataSourceParams;
+import android.tracing.perfetto.InitArguments;
+import android.tracing.perfetto.Producer;
+import android.tracing.transition.TransitionDataSource;
+import android.util.proto.ProtoOutputStream;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class PerfettoTransitionTracer implements TransitionTracer {
+ private final AtomicInteger mActiveTraces = new AtomicInteger(0);
+ private final TransitionDataSource mDataSource =
+ new TransitionDataSource(this.mActiveTraces::incrementAndGet, () -> {},
+ this.mActiveTraces::decrementAndGet);
+
+ PerfettoTransitionTracer() {
+ Producer.init(InitArguments.DEFAULTS);
+ mDataSource.register(DataSourceParams.DEFAULTS);
+ }
+
+ /**
+ * Records key information about a transition that has been sent to Shell to be played.
+ * More information will be appended to the same proto object once the transition is finished or
+ * aborted.
+ * Transition information won't be added to the trace buffer until
+ * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this
+ * transition.
+ *
+ * @param transition The transition that has been sent to Shell.
+ * @param targets Information about the target windows of the transition.
+ */
+ @Override
+ public void logSentTransition(Transition transition, ArrayList<Transition.ChangeInfo> targets) {
+ if (!isTracing()) {
+ return;
+ }
+
+ mDataSource.trace((ctx) -> {
+ final ProtoOutputStream os = ctx.newTracePacket();
+
+ final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+
+ os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId());
+ os.write(PerfettoTrace.ShellTransition.CREATE_TIME_NS,
+ transition.mLogger.mCreateTimeNs);
+ os.write(PerfettoTrace.ShellTransition.SEND_TIME_NS, transition.mLogger.mSendTimeNs);
+ os.write(PerfettoTrace.ShellTransition.START_TRANSACTION_ID,
+ transition.getStartTransaction().getId());
+ os.write(PerfettoTrace.ShellTransition.FINISH_TRANSACTION_ID,
+ transition.getFinishTransaction().getId());
+ os.write(PerfettoTrace.ShellTransition.TYPE, transition.mType);
+ os.write(PerfettoTrace.ShellTransition.FLAGS, transition.getFlags());
+
+ addTransitionTargetsToProto(os, targets);
+
+ os.end(token);
+ });
+ }
+
+ /**
+ * Completes the information dumped in {@link #logSentTransition} for a transition
+ * that has finished or aborted, and add the proto object to the trace buffer.
+ *
+ * @param transition The transition that has finished.
+ */
+ @Override
+ public void logFinishedTransition(Transition transition) {
+ if (!isTracing()) {
+ return;
+ }
+
+ mDataSource.trace((ctx) -> {
+ final ProtoOutputStream os = ctx.newTracePacket();
+
+ final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+ os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId());
+ os.write(PerfettoTrace.ShellTransition.FINISH_TIME_NS,
+ transition.mLogger.mFinishTimeNs);
+ os.end(token);
+ });
+ }
+
+ /**
+ * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer
+ * unless actively tracing.
+ *
+ * @param transition The transition that has been aborted
+ */
+ @Override
+ public void logAbortedTransition(Transition transition) {
+ if (!isTracing()) {
+ return;
+ }
+
+ mDataSource.trace((ctx) -> {
+ final ProtoOutputStream os = ctx.newTracePacket();
+
+ final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+ os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId());
+ os.write(PerfettoTrace.ShellTransition.WM_ABORT_TIME_NS,
+ transition.mLogger.mAbortTimeNs);
+ os.end(token);
+ });
+ }
+
+ @Override
+ public void logRemovingStartingWindow(@NonNull StartingData startingData) {
+ if (!isTracing()) {
+ return;
+ }
+
+ mDataSource.trace((ctx) -> {
+ final ProtoOutputStream os = ctx.newTracePacket();
+
+ final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+ os.write(PerfettoTrace.ShellTransition.ID, startingData.mTransitionId);
+ os.write(PerfettoTrace.ShellTransition.STARTING_WINDOW_REMOVE_TIME_NS,
+ SystemClock.elapsedRealtimeNanos());
+ os.end(token);
+ });
+ }
+
+ @Override
+ public void startTrace(PrintWriter pw) {
+ // No-op
+ }
+
+ @Override
+ public void stopTrace(PrintWriter pw) {
+ // No-op
+ }
+
+ @Override
+ public void saveForBugreport(PrintWriter pw) {
+ // Nothing to do here. Handled by Perfetto.
+ }
+
+ @Override
+ public boolean isTracing() {
+ return mActiveTraces.get() > 0;
+ }
+
+ private void addTransitionTargetsToProto(
+ ProtoOutputStream os,
+ ArrayList<Transition.ChangeInfo> targets
+ ) {
+ for (int i = 0; i < targets.size(); ++i) {
+ final Transition.ChangeInfo target = targets.get(i);
+
+ final int layerId;
+ if (target.mContainer.mSurfaceControl.isValid()) {
+ layerId = target.mContainer.mSurfaceControl.getLayerId();
+ } else {
+ layerId = -1;
+ }
+ final int windowId = System.identityHashCode(target.mContainer);
+
+ final long token = os.start(PerfettoTrace.ShellTransition.TARGETS);
+ os.write(PerfettoTrace.ShellTransition.Target.MODE, target.mReadyMode);
+ os.write(PerfettoTrace.ShellTransition.Target.FLAGS, target.mReadyFlags);
+ os.write(PerfettoTrace.ShellTransition.Target.LAYER_ID, layerId);
+ os.write(PerfettoTrace.ShellTransition.Target.WINDOW_ID, windowId);
+ os.end(token);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
new file mode 100644
index 0000000..5f488b7
--- /dev/null
+++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2024 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.wm;
+
+import static android.content.Context.MEDIA_PROJECTION_SERVICE;
+
+import static com.android.internal.protolog.ProtoLogGroup.WM_ERROR;
+
+import android.media.projection.IMediaProjectionManager;
+import android.media.projection.IMediaProjectionWatcherCallback;
+import android.media.projection.MediaProjectionInfo;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.view.ContentRecordingSession;
+import android.window.IScreenRecordingCallback;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.protolog.common.ProtoLog;
+
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.Set;
+
+public class ScreenRecordingCallbackController {
+
+ private final class Callback implements IBinder.DeathRecipient {
+
+ IScreenRecordingCallback mCallback;
+ int mUid;
+
+ Callback(IScreenRecordingCallback callback, int uid) {
+ this.mCallback = callback;
+ this.mUid = uid;
+ }
+
+ public void binderDied() {
+ unregister(mCallback);
+ }
+ }
+
+ @GuardedBy("WindowManagerService.mGlobalLock")
+ private final Map<IBinder, Callback> mCallbacks = new ArrayMap<>();
+
+ @GuardedBy("WindowManagerService.mGlobalLock")
+ private final Map<Integer /*UID*/, Boolean> mLastInvokedStateByUid = new ArrayMap<>();
+
+ private final WindowManagerService mWms;
+
+ @GuardedBy("WindowManagerService.mGlobalLock")
+ private WindowContainer<WindowContainer> mRecordedWC;
+
+ private boolean mWatcherCallbackRegistered = false;
+
+ private final class MediaProjectionWatcherCallback extends
+ IMediaProjectionWatcherCallback.Stub {
+ @Override
+ public void onStart(MediaProjectionInfo mediaProjectionInfo) {
+ onScreenRecordingStart(mediaProjectionInfo);
+ }
+
+ @Override
+ public void onStop(MediaProjectionInfo mediaProjectionInfo) {
+ onScreenRecordingStop();
+ }
+
+ @Override
+ public void onRecordingSessionSet(MediaProjectionInfo mediaProjectionInfo,
+ ContentRecordingSession contentRecordingSession) {
+ }
+ }
+
+ ScreenRecordingCallbackController(WindowManagerService wms) {
+ mWms = wms;
+ }
+
+ @GuardedBy("WindowManagerService.mGlobalLock")
+ private void setRecordedWindowContainer(MediaProjectionInfo mediaProjectionInfo) {
+ if (mediaProjectionInfo.getLaunchCookie() == null) {
+ mRecordedWC = (WindowContainer) mWms.mRoot.getDefaultDisplay();
+ } else {
+ mRecordedWC = mWms.mRoot.getActivity(activity -> activity.mLaunchCookie
+ == mediaProjectionInfo.getLaunchCookie()).getTask();
+ }
+ }
+
+ @GuardedBy("WindowManagerService.mGlobalLock")
+ private void ensureMediaProjectionWatcherCallbackRegistered() {
+ if (mWatcherCallbackRegistered) {
+ return;
+ }
+
+ IBinder binder = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
+ IMediaProjectionManager mediaProjectionManager =
+ IMediaProjectionManager.Stub.asInterface(binder);
+
+ long identityToken = Binder.clearCallingIdentity();
+ MediaProjectionInfo mediaProjectionInfo = null;
+ try {
+ mediaProjectionInfo = mediaProjectionManager.addCallback(
+ new MediaProjectionWatcherCallback());
+ mWatcherCallbackRegistered = true;
+ } catch (RemoteException e) {
+ ProtoLog.e(WM_ERROR, "Failed to register MediaProjectionWatcherCallback");
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+
+ if (mediaProjectionInfo != null) {
+ setRecordedWindowContainer(mediaProjectionInfo);
+ }
+ }
+
+ boolean register(IScreenRecordingCallback callback) {
+ synchronized (mWms.mGlobalLock) {
+ ensureMediaProjectionWatcherCallbackRegistered();
+
+ IBinder binder = callback.asBinder();
+ int uid = Binder.getCallingUid();
+
+ if (mCallbacks.containsKey(binder)) {
+ return mLastInvokedStateByUid.get(uid);
+ }
+
+ Callback callbackInfo = new Callback(callback, uid);
+ try {
+ binder.linkToDeath(callbackInfo, 0);
+ } catch (RemoteException e) {
+ return false;
+ }
+
+ boolean uidInRecording = uidHasRecordedActivity(callbackInfo.mUid);
+ mLastInvokedStateByUid.put(callbackInfo.mUid, uidInRecording);
+ mCallbacks.put(binder, callbackInfo);
+ return uidInRecording;
+ }
+ }
+
+ void unregister(IScreenRecordingCallback callback) {
+ synchronized (mWms.mGlobalLock) {
+ IBinder binder = callback.asBinder();
+ Callback callbackInfo = mCallbacks.remove(binder);
+ binder.unlinkToDeath(callbackInfo, 0);
+
+ boolean uidHasCallback = false;
+ for (Callback cb : mCallbacks.values()) {
+ if (cb.mUid == callbackInfo.mUid) {
+ uidHasCallback = true;
+ break;
+ }
+ }
+ if (!uidHasCallback) {
+ mLastInvokedStateByUid.remove(callbackInfo.mUid);
+ }
+ }
+ }
+
+ private void onScreenRecordingStart(MediaProjectionInfo mediaProjectionInfo) {
+ synchronized (mWms.mGlobalLock) {
+ setRecordedWindowContainer(mediaProjectionInfo);
+ dispatchCallbacks(getRecordedUids(), true /* visibleInScreenRecording*/);
+ }
+ }
+
+ private void onScreenRecordingStop() {
+ synchronized (mWms.mGlobalLock) {
+ dispatchCallbacks(getRecordedUids(), false /*visibleInScreenRecording*/);
+ mRecordedWC = null;
+ }
+ }
+
+ @GuardedBy("WindowManagerService.mGlobalLock")
+ void onProcessActivityVisibilityChanged(int uid, boolean processVisible) {
+ // If recording isn't active or there's no registered callback for the uid, there's nothing
+ // to do on this visibility change.
+ if (mRecordedWC == null || !mLastInvokedStateByUid.containsKey(uid)) {
+ return;
+ }
+
+ // If the callbacks are already in the correct state, avoid making duplicate callbacks for
+ // the same state. This can happen when:
+ // * a process becomes visible but its UID already has a recorded activity from another
+ // process.
+ // * a process becomes invisible but its UID already doesn't have any recorded activities.
+ if (processVisible == mLastInvokedStateByUid.get(uid)) {
+ return;
+ }
+
+ // If the process visibility change doesn't change the visibility of the UID, avoid making
+ // duplicate callbacks for the same state. This can happen when:
+ // * a process becomes visible but the newly visible activity isn't in the recorded window
+ // container.
+ // * a process becomes invisible but there are still activities being recorded for the UID.
+ boolean uidInRecording = uidHasRecordedActivity(uid);
+ if ((processVisible && !uidInRecording) || (!processVisible && uidInRecording)) {
+ return;
+ }
+
+ dispatchCallbacks(Set.of(uid), processVisible);
+ }
+
+ @GuardedBy("WindowManagerService.mGlobalLock")
+ private boolean uidHasRecordedActivity(int uid) {
+ if (mRecordedWC == null) {
+ return false;
+ }
+ boolean[] hasRecordedActivity = {false};
+ mRecordedWC.forAllActivities(activityRecord -> {
+ if (activityRecord.getUid() == uid && activityRecord.isVisibleRequested()) {
+ hasRecordedActivity[0] = true;
+ return true;
+ }
+ return false;
+ }, true /*traverseTopToBottom*/);
+ return hasRecordedActivity[0];
+ }
+
+ @GuardedBy("WindowManagerService.mGlobalLock")
+ private Set<Integer> getRecordedUids() {
+ Set<Integer> result = new ArraySet<>();
+ if (mRecordedWC == null) {
+ return result;
+ }
+ mRecordedWC.forAllActivities(activityRecord -> {
+ if (activityRecord.isVisibleRequested() && mLastInvokedStateByUid.containsKey(
+ activityRecord.getUid())) {
+ result.add(activityRecord.getUid());
+ }
+ }, true /*traverseTopToBottom*/);
+ return result;
+ }
+
+ @GuardedBy("WindowManagerService.mGlobalLock")
+ private void dispatchCallbacks(Set<Integer> uids, boolean visibleInScreenRecording) {
+ if (uids.isEmpty()) {
+ return;
+ }
+
+ for (Integer uid : uids) {
+ mLastInvokedStateByUid.put(uid, visibleInScreenRecording);
+ }
+
+ for (Callback callback : mCallbacks.values()) {
+ if (!uids.contains(callback.mUid)) {
+ continue;
+ }
+ try {
+ callback.mCallback.onScreenRecordingStateChanged(visibleInScreenRecording);
+ } catch (RemoteException e) {
+ // Client has died. Cleanup is handled via DeathRecipient.
+ }
+ }
+ }
+
+ void dump(PrintWriter pw) {
+ pw.format("ScreenRecordingCallbackController:\n");
+ pw.format(" Registered callbacks:\n");
+ for (Map.Entry<IBinder, Callback> entry : mCallbacks.entrySet()) {
+ pw.format(" callback=%s uid=%s\n", entry.getKey(), entry.getValue().mUid);
+ }
+ pw.format(" Last invoked states:\n");
+ for (Map.Entry<Integer, Boolean> entry : mLastInvokedStateByUid.entrySet()) {
+ pw.format(" uid=%s isVisibleInScreenRecording=%s\n", entry.getKey(),
+ entry.getValue());
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index a7a6bf2..314d720 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -208,6 +208,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -1702,6 +1703,8 @@
final ActivityRecord r = findActivityInHistory(newR.mActivityComponent, newR.mUserId);
if (r == null) return null;
+ moveTaskFragmentsToBottomIfNeeded(r, finishCount);
+
final PooledPredicate f = PooledLambda.obtainPredicate(
(ActivityRecord ar, ActivityRecord boundaryActivity) ->
finishActivityAbove(ar, boundaryActivity, finishCount),
@@ -1722,6 +1725,50 @@
return r;
}
+ /**
+ * Moves {@link TaskFragment}s to the bottom if the flag
+ * {@link TaskFragment#isMoveToBottomIfClearWhenLaunch} is {@code true}.
+ */
+ @VisibleForTesting
+ void moveTaskFragmentsToBottomIfNeeded(@NonNull ActivityRecord r, @NonNull int[] finishCount) {
+ final int activityIndex = mChildren.indexOf(r);
+ if (activityIndex < 0) {
+ return;
+ }
+
+ List<TaskFragment> taskFragmentsToMove = null;
+
+ // Find the TaskFragments that need to be moved
+ for (int i = mChildren.size() - 1; i > activityIndex; i--) {
+ final TaskFragment taskFragment = mChildren.get(i).asTaskFragment();
+ if (taskFragment != null && taskFragment.isMoveToBottomIfClearWhenLaunch()) {
+ if (taskFragmentsToMove == null) {
+ taskFragmentsToMove = new ArrayList<>();
+ }
+ taskFragmentsToMove.add(taskFragment);
+ }
+ }
+ if (taskFragmentsToMove == null) {
+ return;
+ }
+
+ // Move the TaskFragments to the bottom of the Task. Their relative orders are preserved.
+ final int size = taskFragmentsToMove.size();
+ for (int i = 0; i < size; i++) {
+ final TaskFragment taskFragment = taskFragmentsToMove.get(i);
+
+ // The visibility of the TaskFragment may change. Collect it in the transition so that
+ // transition animation can be properly played.
+ mTransitionController.collect(taskFragment);
+
+ positionChildAt(POSITION_BOTTOM, taskFragment, false /* includeParents */);
+ }
+
+ // Treat it as if the TaskFragments are finished so that a transition animation can be
+ // played to send the TaskFragments back and bring the activity to front.
+ finishCount[0] += size;
+ }
+
private static boolean finishActivityAbove(ActivityRecord r, ActivityRecord boundaryActivity,
@NonNull int[] finishCount) {
// Stop operation once we reach the boundary activity.
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index f56759f..7d418ea 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -363,6 +363,12 @@
*/
private boolean mIsolatedNav;
+ /**
+ * Whether the TaskFragment should move to bottom of task when any activity below it is
+ * launched in clear top mode.
+ */
+ private boolean mMoveToBottomIfClearWhenLaunch;
+
/** When set, will force the task to report as invisible. */
static final int FLAG_FORCE_HIDDEN_FOR_PINNED_TASK = 1;
static final int FLAG_FORCE_HIDDEN_FOR_TASK_ORG = 1 << 1;
@@ -3045,6 +3051,14 @@
mEmbeddedDimArea = embeddedDimArea;
}
+ void setMoveToBottomIfClearWhenLaunch(boolean moveToBottomIfClearWhenLaunch) {
+ mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch;
+ }
+
+ boolean isMoveToBottomIfClearWhenLaunch() {
+ return mMoveToBottomIfClearWhenLaunch;
+ }
+
@VisibleForTesting
boolean isDimmingOnParentTask() {
return mEmbeddedDimArea == EMBEDDED_DIM_AREA_PARENT_TASK;
diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java
index c59d2d3..0f3fe22 100644
--- a/services/core/java/com/android/server/wm/TransitionTracer.java
+++ b/services/core/java/com/android/server/wm/TransitionTracer.java
@@ -1,323 +1,19 @@
-/*
- * 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.wm;
-import static android.os.Build.IS_USER;
-
-import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER;
-import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_H;
-import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_L;
-import static com.android.server.wm.shell.TransitionTraceProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.os.SystemClock;
-import android.os.Trace;
-import android.util.Log;
-import android.util.proto.ProtoOutputStream;
-import com.android.internal.util.TraceBuffer;
-import com.android.server.wm.Transition.ChangeInfo;
-
-import java.io.File;
-import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.concurrent.TimeUnit;
-/**
- * Helper class to collect and dump transition traces.
- */
-public class TransitionTracer {
+interface TransitionTracer {
+ void logSentTransition(Transition transition, ArrayList<Transition.ChangeInfo> targets);
+ void logFinishedTransition(Transition transition);
+ void logAbortedTransition(Transition transition);
+ void logRemovingStartingWindow(@NonNull StartingData startingData);
- private static final String LOG_TAG = "TransitionTracer";
-
- private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB
- private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB
-
- // This will be the size the proto output streams are initialized to.
- // Ideally this should fit most or all the proto objects we will create and be no bigger than
- // that to ensure to don't use excessive amounts of memory.
- private static final int CHUNK_SIZE = 64;
-
- static final String WINSCOPE_EXT = ".winscope";
- private static final String TRACE_FILE =
- "/data/misc/wmtrace/wm_transition_trace" + WINSCOPE_EXT;
- private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
-
- private final TraceBuffer mTraceBuffer = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY);
-
- private final Object mEnabledLock = new Object();
- private volatile boolean mActiveTracingEnabled = false;
-
- /**
- * Records key information about a transition that has been sent to Shell to be played.
- * More information will be appended to the same proto object once the transition is finished or
- * aborted.
- * Transition information won't be added to the trace buffer until
- * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this
- * transition.
- *
- * @param transition The transition that has been sent to Shell.
- * @param targets Information about the target windows of the transition.
- */
- public void logSentTransition(Transition transition, ArrayList<ChangeInfo> targets) {
- try {
- final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
- final long protoToken = outputStream
- .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
- outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
- outputStream.write(com.android.server.wm.shell.Transition.CREATE_TIME_NS,
- transition.mLogger.mCreateTimeNs);
- outputStream.write(com.android.server.wm.shell.Transition.SEND_TIME_NS,
- transition.mLogger.mSendTimeNs);
- outputStream.write(com.android.server.wm.shell.Transition.START_TRANSACTION_ID,
- transition.getStartTransaction().getId());
- outputStream.write(com.android.server.wm.shell.Transition.FINISH_TRANSACTION_ID,
- transition.getFinishTransaction().getId());
- dumpTransitionTargetsToProto(outputStream, transition, targets);
- outputStream.end(protoToken);
-
- mTraceBuffer.add(outputStream);
- } catch (Exception e) {
- // Don't let any errors in the tracing cause the transition to fail
- Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
- }
- }
-
- /**
- * Completes the information dumped in {@link #logSentTransition} for a transition
- * that has finished or aborted, and add the proto object to the trace buffer.
- *
- * @param transition The transition that has finished.
- */
- public void logFinishedTransition(Transition transition) {
- try {
- final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
- final long protoToken = outputStream
- .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
- outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
- outputStream.write(com.android.server.wm.shell.Transition.FINISH_TIME_NS,
- transition.mLogger.mFinishTimeNs);
- outputStream.end(protoToken);
-
- mTraceBuffer.add(outputStream);
- } catch (Exception e) {
- // Don't let any errors in the tracing cause the transition to fail
- Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
- }
- }
-
- /**
- * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer
- * unless actively tracing.
- *
- * @param transition The transition that has been aborted
- */
- public void logAbortedTransition(Transition transition) {
- try {
- final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
- final long protoToken = outputStream
- .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
- outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
- outputStream.write(com.android.server.wm.shell.Transition.ABORT_TIME_NS,
- transition.mLogger.mAbortTimeNs);
- outputStream.end(protoToken);
-
- mTraceBuffer.add(outputStream);
- } catch (Exception e) {
- // Don't let any errors in the tracing cause the transition to fail
- Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
- }
- }
-
- void logRemovingStartingWindow(@NonNull StartingData startingData) {
- if (startingData.mTransitionId == 0) {
- return;
- }
- try {
- final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
- final long protoToken = outputStream
- .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
- outputStream.write(com.android.server.wm.shell.Transition.ID,
- startingData.mTransitionId);
- outputStream.write(
- com.android.server.wm.shell.Transition.STARTING_WINDOW_REMOVE_TIME_NS,
- SystemClock.elapsedRealtimeNanos());
- outputStream.end(protoToken);
-
- mTraceBuffer.add(outputStream);
- } catch (Exception e) {
- Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
- }
- }
-
- private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream,
- Transition transition, ArrayList<ChangeInfo> targets) {
- Trace.beginSection("TransitionTracer#dumpTransitionTargetsToProto");
- if (mActiveTracingEnabled) {
- outputStream.write(com.android.server.wm.shell.Transition.ID,
- transition.getSyncId());
- }
-
- outputStream.write(com.android.server.wm.shell.Transition.TYPE, transition.mType);
- outputStream.write(com.android.server.wm.shell.Transition.FLAGS, transition.getFlags());
-
- for (int i = 0; i < targets.size(); ++i) {
- final long changeToken = outputStream
- .start(com.android.server.wm.shell.Transition.TARGETS);
-
- final Transition.ChangeInfo target = targets.get(i);
-
- final int layerId;
- if (target.mContainer.mSurfaceControl.isValid()) {
- layerId = target.mContainer.mSurfaceControl.getLayerId();
- } else {
- layerId = -1;
- }
-
- outputStream.write(com.android.server.wm.shell.Target.MODE, target.mReadyMode);
- outputStream.write(com.android.server.wm.shell.Target.FLAGS, target.mReadyFlags);
- outputStream.write(com.android.server.wm.shell.Target.LAYER_ID, layerId);
-
- if (mActiveTracingEnabled) {
- // What we use in the WM trace
- final int windowId = System.identityHashCode(target.mContainer);
- outputStream.write(com.android.server.wm.shell.Target.WINDOW_ID, windowId);
- }
-
- outputStream.end(changeToken);
- }
-
- Trace.endSection();
- }
-
- /**
- * Starts collecting transitions for the trace.
- * If called while a trace is already running, this will reset the trace.
- */
- public void startTrace(@Nullable PrintWriter pw) {
- if (IS_USER) {
- LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
- return;
- }
- Trace.beginSection("TransitionTracer#startTrace");
- LogAndPrintln.i(pw, "Starting shell transition trace.");
- synchronized (mEnabledLock) {
- mActiveTracingEnabled = true;
- mTraceBuffer.resetBuffer();
- mTraceBuffer.setCapacity(ACTIVE_TRACING_BUFFER_CAPACITY);
- }
- Trace.endSection();
- }
-
- /**
- * Stops collecting the transition trace and dump to trace to file.
- *
- * Dumps the trace to @link{TRACE_FILE}.
- */
- public void stopTrace(@Nullable PrintWriter pw) {
- stopTrace(pw, new File(TRACE_FILE));
- }
-
- /**
- * Stops collecting the transition trace and dump to trace to file.
- * @param outputFile The file to dump the transition trace to.
- */
- public void stopTrace(@Nullable PrintWriter pw, File outputFile) {
- if (IS_USER) {
- LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
- return;
- }
- Trace.beginSection("TransitionTracer#stopTrace");
- LogAndPrintln.i(pw, "Stopping shell transition trace.");
- synchronized (mEnabledLock) {
- mActiveTracingEnabled = false;
- writeTraceToFileLocked(pw, outputFile);
- mTraceBuffer.resetBuffer();
- mTraceBuffer.setCapacity(ALWAYS_ON_TRACING_CAPACITY);
- }
- Trace.endSection();
- }
-
- /**
- * Being called while taking a bugreport so that tracing files can be included in the bugreport.
- *
- * @param pw Print writer
- */
- public void saveForBugreport(@Nullable PrintWriter pw) {
- if (IS_USER) {
- LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
- return;
- }
- Trace.beginSection("TransitionTracer#saveForBugreport");
- synchronized (mEnabledLock) {
- final File outputFile = new File(TRACE_FILE);
- writeTraceToFileLocked(pw, outputFile);
- }
- Trace.endSection();
- }
-
- boolean isActiveTracingEnabled() {
- return mActiveTracingEnabled;
- }
-
- private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) {
- Trace.beginSection("TransitionTracer#writeTraceToFileLocked");
- try {
- ProtoOutputStream proto = new ProtoOutputStream(CHUNK_SIZE);
- proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
- long timeOffsetNs =
- TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
- - SystemClock.elapsedRealtimeNanos();
- proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
- int pid = android.os.Process.myPid();
- LogAndPrintln.i(pw, "Writing file to " + file.getAbsolutePath()
- + " from process " + pid);
- mTraceBuffer.writeTraceToFile(file, proto);
- } catch (IOException e) {
- LogAndPrintln.e(pw, "Unable to write buffer to file", e);
- }
- Trace.endSection();
- }
-
- private static class LogAndPrintln {
- private static void i(@Nullable PrintWriter pw, String msg) {
- Log.i(LOG_TAG, msg);
- if (pw != null) {
- pw.println(msg);
- pw.flush();
- }
- }
-
- private static void e(@Nullable PrintWriter pw, String msg) {
- Log.e(LOG_TAG, msg);
- if (pw != null) {
- pw.println("ERROR: " + msg);
- pw.flush();
- }
- }
-
- private static void e(@Nullable PrintWriter pw, String msg, @NonNull Exception e) {
- Log.e(LOG_TAG, msg, e);
- if (pw != null) {
- pw.println("ERROR: " + msg + " ::\n " + e);
- pw.flush();
- }
- }
- }
+ void startTrace(@Nullable PrintWriter pw);
+ void stopTrace(@Nullable PrintWriter pw);
+ boolean isTracing();
+ void saveForBugreport(@Nullable PrintWriter pw);
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 9544835..9179acf 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -303,6 +303,7 @@
import android.view.inputmethod.ImeTracker;
import android.window.AddToSurfaceSyncGroupResult;
import android.window.ClientWindowFrames;
+import android.window.IScreenRecordingCallback;
import android.window.ISurfaceSyncGroupCompletedListener;
import android.window.ITaskFpsCallback;
import android.window.ITrustedPresentationListener;
@@ -1104,6 +1105,8 @@
void onAppFreezeTimeout();
}
+ private final ScreenRecordingCallbackController mScreenRecordingCallbackController;
+
public static WindowManagerService main(final Context context, final InputManagerService im,
final boolean showBootMsgs, WindowManagerPolicy policy,
ActivityTaskManagerService atm) {
@@ -1213,7 +1216,12 @@
mWindowTracing = WindowTracing.createDefaultAndStartLooper(this,
Choreographer.getInstance());
- mTransitionTracer = new TransitionTracer();
+
+ if (android.tracing.Flags.perfettoTransitionTracing()) {
+ mTransitionTracer = new PerfettoTransitionTracer();
+ } else {
+ mTransitionTracer = new LegacyTransitionTracer();
+ }
LocalServices.addService(WindowManagerPolicy.class, mPolicy);
@@ -1340,6 +1348,7 @@
mBlurController = new BlurController(mContext, mPowerManager);
mTaskFpsCallbackController = new TaskFpsCallbackController(mContext);
mAccessibilityController = new AccessibilityController(this);
+ mScreenRecordingCallbackController = new ScreenRecordingCallbackController(this);
mSystemPerformanceHinter = new SystemPerformanceHinter(mContext, displayId -> {
synchronized (mGlobalLock) {
DisplayContent dc = mRoot.getDisplayContent(displayId);
@@ -6087,7 +6096,7 @@
@Override
public boolean isTransitionTraceEnabled() {
- return mTransitionTracer.isActiveTracingEnabled();
+ return mTransitionTracer.isTracing();
}
@Override
@@ -7183,6 +7192,7 @@
mSystemPerformanceHinter.dump(pw, "");
mTrustedPresentationListenerController.dump(pw);
mSensitiveContentPackages.dump(pw);
+ mScreenRecordingCallbackController.dump(pw);
}
}
@@ -9884,4 +9894,18 @@
int id) {
mTrustedPresentationListenerController.unregisterListener(listener, id);
}
+
+ @Override
+ public boolean registerScreenRecordingCallback(IScreenRecordingCallback callback) {
+ return mScreenRecordingCallbackController.register(callback);
+ }
+
+ @Override
+ public void unregisterScreenRecordingCallback(IScreenRecordingCallback callback) {
+ mScreenRecordingCallbackController.unregister(callback);
+ }
+
+ void onProcessActivityVisibilityChanged(int uid, boolean visible) {
+ mScreenRecordingCallbackController.onProcessActivityVisibilityChanged(uid, visible);
+ }
}
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 0da0bb4..205ed97 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -36,6 +36,7 @@
import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS;
import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN;
@@ -1514,6 +1515,11 @@
: EMBEDDED_DIM_AREA_TASK_FRAGMENT);
break;
}
+ case OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH: {
+ taskFragment.setMoveToBottomIfClearWhenLaunch(
+ operation.isMoveToBottomIfClearWhenLaunch());
+ break;
+ }
}
return effects;
}
@@ -1566,6 +1572,17 @@
return false;
}
+ if ((opType == OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH)
+ && !mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) {
+ final Throwable exception = new SecurityException(
+ "Only a system organizer can perform "
+ + "OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH."
+ );
+ sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment,
+ opType, exception);
+ return false;
+ }
+
final IBinder secondaryFragmentToken = operation.getSecondaryFragmentToken();
return secondaryFragmentToken == null
|| validateTaskFragment(mLaunchTaskFragments.get(secondaryFragmentToken), opType,
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index b8fa5e5..6d2e8cc 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -1271,8 +1271,10 @@
& (ACTIVITY_STATE_FLAG_IS_VISIBLE | ACTIVITY_STATE_FLAG_IS_WINDOW_VISIBLE)) != 0;
if (!wasAnyVisible && anyVisible) {
mAtm.mVisibleActivityProcessTracker.onAnyActivityVisible(this);
+ mAtm.mWindowManager.onProcessActivityVisibilityChanged(mUid, true /*visible*/);
} else if (wasAnyVisible && !anyVisible) {
mAtm.mVisibleActivityProcessTracker.onAllActivitiesInvisible(this);
+ mAtm.mWindowManager.onProcessActivityVisibilityChanged(mUid, false /*visible*/);
} else if (wasAnyVisible && !wasResumed && hasResumedActivity()) {
mAtm.mVisibleActivityProcessTracker.onActivityResumedWhileVisible(this);
}
diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
index 4cd018b..50d48b7 100644
--- a/services/core/jni/com_android_server_companion_virtual_InputController.cpp
+++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
@@ -44,6 +44,7 @@
MOUSE,
TOUCHSCREEN,
DPAD,
+ STYLUS,
};
static unique_fd invalidFd() {
@@ -98,6 +99,24 @@
ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOUCH_MAJOR);
ioctl(fd, UI_SET_ABSBIT, ABS_MT_PRESSURE);
ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
+ break;
+ case DeviceType::STYLUS:
+ ioctl(fd, UI_SET_EVBIT, EV_ABS);
+ ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH);
+ ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS);
+ ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS2);
+ ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_PEN);
+ ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_RUBBER);
+ ioctl(fd, UI_SET_ABSBIT, ABS_X);
+ ioctl(fd, UI_SET_ABSBIT, ABS_Y);
+ ioctl(fd, UI_SET_ABSBIT, ABS_TILT_X);
+ ioctl(fd, UI_SET_ABSBIT, ABS_TILT_Y);
+ ioctl(fd, UI_SET_ABSBIT, ABS_PRESSURE);
+ ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
+ break;
+ default:
+ ALOGE("Invalid input device type %d", static_cast<int32_t>(deviceType));
+ return invalidFd();
}
int version;
@@ -158,6 +177,47 @@
ALOGE("Error creating touchscreen uinput tracking ids: %s", strerror(errno));
return invalidFd();
}
+ } else if (deviceType == DeviceType::STYLUS) {
+ uinput_abs_setup xAbsSetup;
+ xAbsSetup.code = ABS_X;
+ xAbsSetup.absinfo.maximum = screenWidth - 1;
+ xAbsSetup.absinfo.minimum = 0;
+ if (ioctl(fd, UI_ABS_SETUP, &xAbsSetup) != 0) {
+ ALOGE("Error creating stylus uinput x axis: %s", strerror(errno));
+ return invalidFd();
+ }
+ uinput_abs_setup yAbsSetup;
+ yAbsSetup.code = ABS_Y;
+ yAbsSetup.absinfo.maximum = screenHeight - 1;
+ yAbsSetup.absinfo.minimum = 0;
+ if (ioctl(fd, UI_ABS_SETUP, &yAbsSetup) != 0) {
+ ALOGE("Error creating stylus uinput y axis: %s", strerror(errno));
+ return invalidFd();
+ }
+ uinput_abs_setup tiltXAbsSetup;
+ tiltXAbsSetup.code = ABS_TILT_X;
+ tiltXAbsSetup.absinfo.maximum = 90;
+ tiltXAbsSetup.absinfo.minimum = -90;
+ if (ioctl(fd, UI_ABS_SETUP, &tiltXAbsSetup) != 0) {
+ ALOGE("Error creating stylus uinput tilt x axis: %s", strerror(errno));
+ return invalidFd();
+ }
+ uinput_abs_setup tiltYAbsSetup;
+ tiltYAbsSetup.code = ABS_TILT_Y;
+ tiltYAbsSetup.absinfo.maximum = 90;
+ tiltYAbsSetup.absinfo.minimum = -90;
+ if (ioctl(fd, UI_ABS_SETUP, &tiltYAbsSetup) != 0) {
+ ALOGE("Error creating stylus uinput tilt y axis: %s", strerror(errno));
+ return invalidFd();
+ }
+ uinput_abs_setup pressureAbsSetup;
+ pressureAbsSetup.code = ABS_PRESSURE;
+ pressureAbsSetup.absinfo.maximum = 255;
+ pressureAbsSetup.absinfo.minimum = 0;
+ if (ioctl(fd, UI_ABS_SETUP, &pressureAbsSetup) != 0) {
+ ALOGE("Error creating touchscreen uinput pressure axis: %s", strerror(errno));
+ return invalidFd();
+ }
}
if (ioctl(fd, UI_DEV_SETUP, &setup) != 0) {
ALOGE("Error creating uinput device: %s", strerror(errno));
@@ -182,6 +242,17 @@
fallback.absmax[ABS_MT_TOUCH_MAJOR] = screenWidth - 1;
fallback.absmin[ABS_MT_PRESSURE] = 0;
fallback.absmax[ABS_MT_PRESSURE] = 255;
+ } else if (deviceType == DeviceType::STYLUS) {
+ fallback.absmin[ABS_X] = 0;
+ fallback.absmax[ABS_X] = screenWidth - 1;
+ fallback.absmin[ABS_Y] = 0;
+ fallback.absmax[ABS_Y] = screenHeight - 1;
+ fallback.absmin[ABS_TILT_X] = -90;
+ fallback.absmax[ABS_TILT_X] = 90;
+ fallback.absmin[ABS_TILT_Y] = -90;
+ fallback.absmax[ABS_TILT_Y] = 90;
+ fallback.absmin[ABS_PRESSURE] = 0;
+ fallback.absmax[ABS_PRESSURE] = 255;
}
if (TEMP_FAILURE_RETRY(write(fd, &fallback, sizeof(fallback))) != sizeof(fallback)) {
ALOGE("Error creating uinput device: %s", strerror(errno));
@@ -234,6 +305,13 @@
return fd.ok() ? reinterpret_cast<jlong>(new VirtualTouchscreen(std::move(fd))) : INVALID_PTR;
}
+static jlong nativeOpenUinputStylus(JNIEnv* env, jobject thiz, jstring name, jint vendorId,
+ jint productId, jstring phys, jint height, jint width) {
+ auto fd =
+ openUinputJni(env, name, vendorId, productId, phys, DeviceType::STYLUS, height, width);
+ return fd.ok() ? reinterpret_cast<jlong>(new VirtualStylus(std::move(fd))) : INVALID_PTR;
+}
+
static void nativeCloseUinput(JNIEnv* env, jobject thiz, jlong ptr) {
VirtualInputDevice* virtualInputDevice = reinterpret_cast<VirtualInputDevice*>(ptr);
delete virtualInputDevice;
@@ -287,6 +365,22 @@
std::chrono::nanoseconds(eventTimeNanos));
}
+// Native methods for VirtualStylus
+static bool nativeWriteStylusMotionEvent(JNIEnv* env, jobject thiz, jlong ptr, jint toolType,
+ jint action, jint locationX, jint locationY, jint pressure,
+ jint tiltX, jint tiltY, jlong eventTimeNanos) {
+ VirtualStylus* virtualStylus = reinterpret_cast<VirtualStylus*>(ptr);
+ return virtualStylus->writeMotionEvent(toolType, action, locationX, locationY, pressure, tiltX,
+ tiltY, std::chrono::nanoseconds(eventTimeNanos));
+}
+
+static bool nativeWriteStylusButtonEvent(JNIEnv* env, jobject thiz, jlong ptr, jint buttonCode,
+ jint action, jlong eventTimeNanos) {
+ VirtualStylus* virtualStylus = reinterpret_cast<VirtualStylus*>(ptr);
+ return virtualStylus->writeButtonEvent(buttonCode, action,
+ std::chrono::nanoseconds(eventTimeNanos));
+}
+
static JNINativeMethod methods[] = {
{"nativeOpenUinputDpad", "(Ljava/lang/String;IILjava/lang/String;)J",
(void*)nativeOpenUinputDpad},
@@ -296,6 +390,8 @@
(void*)nativeOpenUinputMouse},
{"nativeOpenUinputTouchscreen", "(Ljava/lang/String;IILjava/lang/String;II)J",
(void*)nativeOpenUinputTouchscreen},
+ {"nativeOpenUinputStylus", "(Ljava/lang/String;IILjava/lang/String;II)J",
+ (void*)nativeOpenUinputStylus},
{"nativeCloseUinput", "(J)V", (void*)nativeCloseUinput},
{"nativeWriteDpadKeyEvent", "(JIIJ)Z", (void*)nativeWriteDpadKeyEvent},
{"nativeWriteKeyEvent", "(JIIJ)Z", (void*)nativeWriteKeyEvent},
@@ -303,6 +399,8 @@
{"nativeWriteTouchEvent", "(JIIIFFFFJ)Z", (void*)nativeWriteTouchEvent},
{"nativeWriteRelativeEvent", "(JFFJ)Z", (void*)nativeWriteRelativeEvent},
{"nativeWriteScrollEvent", "(JFFJ)Z", (void*)nativeWriteScrollEvent},
+ {"nativeWriteStylusMotionEvent", "(JIIIIIIIJ)Z", (void*)nativeWriteStylusMotionEvent},
+ {"nativeWriteStylusButtonEvent", "(JIIJ)Z", (void*)nativeWriteStylusButtonEvent},
};
int register_android_server_companion_virtual_InputController(JNIEnv* env) {
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 3cbceec..a469165 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -625,6 +625,9 @@
If no mode is specified, the mapping will be used for the default mode.
If no setting is specified, the mapping will be used for the normal brightness setting.
+
+ If no mapping is defined for one of the settings, the mapping for the normal setting will be
+ used as a fallback.
-->
<xs:complexType name="luxToBrightnessMapping">
<xs:element name="map" type="nonNegativeFloatToFloatMap">
diff --git a/services/foldables/devicestateprovider/proguard.flags b/services/foldables/devicestateprovider/proguard.flags
index 069cbc6..b810cad 100644
--- a/services/foldables/devicestateprovider/proguard.flags
+++ b/services/foldables/devicestateprovider/proguard.flags
@@ -1 +1 @@
--keep,allowoptimization,allowaccessmodification class com.android.server.policy.TentModeDeviceStatePolicy { *; }
+-keep,allowoptimization,allowaccessmodification class com.android.server.policy.BookStyleDeviceStatePolicy { *; }
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
new file mode 100644
index 0000000..d5a3cff
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2023 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.policy;
+
+import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen.OUTER;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_0;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_0_TO_45;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_45_TO_90;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_90_TO_180;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.util.ArraySet;
+import android.view.Display;
+import android.view.Surface;
+
+import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen;
+import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle;
+import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition;
+import com.android.server.policy.BookStyleClosedStatePredicate.ConditionSensorListener.SensorSubscription;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * 'Closed' state predicate that takes into account the posture of the device
+ * It accepts list of state transitions that control how the device moves between
+ * device states.
+ * See {@link BookStyleStateTransitions} for detailed description of the default behavior.
+ */
+public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceStateProvider>,
+ DisplayManager.DisplayListener {
+
+ private final BookStylePreferredScreenCalculator mClosedStateCalculator;
+ private final Handler mHandler = new Handler();
+ private final PostureEstimator mPostureEstimator;
+ private final DisplayManager mDisplayManager;
+
+ /**
+ * Creates {@link BookStyleClosedStatePredicate}. It is expected that the device has a pair
+ * of accelerometer sensors (one for each movable part of the device), see parameter
+ * descriptions for the behaviour when these sensors are not available.
+ * @param context context that could be used to get system services
+ * @param updatesListener callback that will be executed whenever the predicate should be
+ * checked again
+ * @param leftAccelerometerSensor accelerometer sensor that is located in the half of the
+ * device that has the outer screen, in case if this sensor is
+ * not provided, tent/wedge mode will be detected only using
+ * orientation sensor and screen rotation, so this mode won't
+ * be accessible by putting the device on a flat surface
+ * @param rightAccelerometerSensor accelerometer sensor that is located on the opposite side
+ * across the hinge from the previous accelerometer sensor,
+ * in case if this sensor is not provided, reverse wedge mode
+ * won't be detected, so the device will use closed state using
+ * constant angle when folding
+ * @param stateTransitions definition of all possible state transitions, see
+ * {@link BookStyleStateTransitions} for sample and more details
+ */
+
+ public BookStyleClosedStatePredicate(@NonNull Context context,
+ @NonNull ClosedStateUpdatesListener updatesListener,
+ @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+ @NonNull List<StateTransition> stateTransitions) {
+ mDisplayManager = context.getSystemService(DisplayManager.class);
+ mDisplayManager.registerDisplayListener(this, mHandler);
+
+ mClosedStateCalculator = new BookStylePreferredScreenCalculator(stateTransitions);
+
+ final SensorManager sensorManager = context.getSystemService(SensorManager.class);
+ final Sensor orientationSensor = sensorManager.getDefaultSensor(
+ Sensor.TYPE_DEVICE_ORIENTATION);
+
+ mPostureEstimator = new PostureEstimator(mHandler, sensorManager,
+ leftAccelerometerSensor, rightAccelerometerSensor, orientationSensor,
+ updatesListener::onClosedStateUpdated);
+ }
+
+ /**
+ * Based on the current sensor readings and current state, returns true if the device should use
+ * 'CLOSED' device state and false if it should not use 'CLOSED' state (e.g. could use half-open
+ * or open states).
+ */
+ @Override
+ public boolean test(FoldableDeviceStateProvider foldableDeviceStateProvider) {
+ final HingeAngle hingeAngle = hingeAngleFromFloat(
+ foldableDeviceStateProvider.getHingeAngle());
+
+ mPostureEstimator.onDeviceClosedStatusChanged(hingeAngle == ANGLE_0);
+
+ final PreferredScreen preferredScreen = mClosedStateCalculator.
+ calculatePreferredScreen(hingeAngle, mPostureEstimator.isLikelyTentOrWedgeMode(),
+ mPostureEstimator.isLikelyReverseWedgeMode(hingeAngle));
+
+ return preferredScreen == OUTER;
+ }
+
+ private HingeAngle hingeAngleFromFloat(float hingeAngle) {
+ if (hingeAngle == 0f) {
+ return ANGLE_0;
+ } else if (hingeAngle < 45f) {
+ return ANGLE_0_TO_45;
+ } else if (hingeAngle < 90f) {
+ return ANGLE_45_TO_90;
+ } else {
+ return ANGLE_90_TO_180;
+ }
+ }
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (displayId == DEFAULT_DISPLAY) {
+ final Display display = mDisplayManager.getDisplay(displayId);
+ int displayState = display.getState();
+ boolean isDisplayOn = displayState == Display.STATE_ON;
+ mPostureEstimator.onDisplayPowerStatusChanged(isDisplayOn);
+ mPostureEstimator.onDisplayRotationChanged(display.getRotation());
+ }
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {
+
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+
+ }
+
+ public interface ClosedStateUpdatesListener {
+ void onClosedStateUpdated();
+ }
+
+ /**
+ * Estimates if the device is going to enter wedge/tent mode based on the sensor data
+ */
+ private static class PostureEstimator implements SensorEventListener {
+
+
+ private static final int FLAT_INCLINATION_THRESHOLD_DEGREES = 8;
+
+ /**
+ * Alpha parameter of the accelerometer low pass filter: the lower the value, the less high
+ * frequency noise it filter but reduces the latency.
+ */
+ private static final float GRAVITY_VECTOR_LOW_PASS_ALPHA_VALUE = 0.8f;
+
+
+ @Nullable
+ private final Sensor mLeftAccelerometerSensor;
+ @Nullable
+ private final Sensor mRightAccelerometerSensor;
+ private final Sensor mOrientationSensor;
+ private final Runnable mOnSensorUpdatedListener;
+
+ private final ConditionSensorListener mConditionedSensorListener;
+
+ @Nullable
+ private float[] mRightGravityVector;
+
+ @Nullable
+ private float[] mLeftGravityVector;
+
+ @Nullable
+ private Integer mLastScreenRotation;
+
+ @Nullable
+ private SensorEvent mLastDeviceOrientationSensorEvent = null;
+
+ private boolean mScreenTurnedOn = false;
+ private boolean mDeviceClosed = false;
+
+ public PostureEstimator(Handler handler, SensorManager sensorManager,
+ @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+ Sensor orientationSensor, Runnable onSensorUpdated) {
+ mLeftAccelerometerSensor = leftAccelerometerSensor;
+ mRightAccelerometerSensor = rightAccelerometerSensor;
+ mOrientationSensor = orientationSensor;
+
+ mOnSensorUpdatedListener = onSensorUpdated;
+
+ final List<SensorSubscription> sensorSubscriptions = new ArrayList<>();
+ if (mLeftAccelerometerSensor != null) {
+ sensorSubscriptions.add(new SensorSubscription(
+ mLeftAccelerometerSensor,
+ /* allowedToListen= */ () -> mScreenTurnedOn && !mDeviceClosed,
+ /* cleanup= */ () -> mLeftGravityVector = null));
+ }
+
+ if (mRightAccelerometerSensor != null) {
+ sensorSubscriptions.add(new SensorSubscription(
+ mRightAccelerometerSensor,
+ /* allowedToListen= */ () -> mScreenTurnedOn,
+ /* cleanup= */ () -> mRightGravityVector = null));
+ }
+
+ sensorSubscriptions.add(new SensorSubscription(mOrientationSensor,
+ /* allowedToListen= */ () -> mScreenTurnedOn,
+ /* cleanup= */ () -> mLastDeviceOrientationSensorEvent = null));
+
+ mConditionedSensorListener = new ConditionSensorListener(sensorManager, this, handler,
+ sensorSubscriptions);
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (event.sensor == mRightAccelerometerSensor) {
+ if (mRightGravityVector == null) {
+ mRightGravityVector = new float[3];
+ }
+ setNewValueWithHighPassFilter(mRightGravityVector, event.values);
+
+ final boolean isRightMostlyFlat = Objects.equals(
+ isGravityVectorMostlyFlat(mRightGravityVector), Boolean.TRUE);
+
+ if (isRightMostlyFlat) {
+ // Reset orientation sensor when the device becomes flat
+ mLastDeviceOrientationSensorEvent = null;
+ }
+ } else if (event.sensor == mLeftAccelerometerSensor) {
+ if (mLeftGravityVector == null) {
+ mLeftGravityVector = new float[3];
+ }
+ setNewValueWithHighPassFilter(mLeftGravityVector, event.values);
+ } else if (event.sensor == mOrientationSensor) {
+ mLastDeviceOrientationSensorEvent = event;
+ }
+
+ mOnSensorUpdatedListener.run();
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+
+ }
+
+ private void setNewValueWithHighPassFilter(float[] output, float[] newValues) {
+ final float alpha = GRAVITY_VECTOR_LOW_PASS_ALPHA_VALUE;
+ output[0] = alpha * output[0] + (1 - alpha) * newValues[0];
+ output[1] = alpha * output[1] + (1 - alpha) * newValues[1];
+ output[2] = alpha * output[2] + (1 - alpha) * newValues[2];
+ }
+
+ /**
+ * Returns true if the phone likely in reverse wedge mode (when a foldable phone is lying
+ * on the outer screen mostly flat to the ground)
+ */
+ public boolean isLikelyReverseWedgeMode(HingeAngle hingeAngle) {
+ return hingeAngle != ANGLE_0 && Objects.equals(
+ isGravityVectorMostlyFlat(mLeftGravityVector), Boolean.TRUE);
+ }
+
+ /**
+ * Returns true if the phone is likely in tent or wedge mode when unfolding. Tent mode
+ * is detected by checking if the phone is in seascape position, screen is rotated to
+ * landscape or seascape, or if the right side of the device is mostly flat.
+ */
+ public boolean isLikelyTentOrWedgeMode() {
+ boolean isScreenLandscapeOrSeascape = Objects.equals(mLastScreenRotation,
+ Surface.ROTATION_270) || Objects.equals(mLastScreenRotation,
+ Surface.ROTATION_90);
+ if (isScreenLandscapeOrSeascape) {
+ return true;
+ }
+
+ boolean isRightMostlyFlat = Objects.equals(
+ isGravityVectorMostlyFlat(mRightGravityVector), Boolean.TRUE);
+ if (isRightMostlyFlat) {
+ return true;
+ }
+
+ boolean isSensorSeaScape = Objects.equals(getOrientationSensorRotation(),
+ Surface.ROTATION_270);
+ if (isSensorSeaScape) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the passed gravity vector implies that the phone is mostly flat (the
+ * vector is close to be perpendicular to the ground and has a positive Z component).
+ * Returns null if there is no data from the sensor.
+ */
+ private Boolean isGravityVectorMostlyFlat(@Nullable float[] vector) {
+ if (vector == null) return null;
+ if (vector[0] == 0.0f && vector[1] == 0.0f && vector[2] == 0.0f) {
+ // Likely we haven't received the actual data yet, treat it as no data
+ return null;
+ }
+
+ double vectorMagnitude = Math.sqrt(
+ vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
+ float normalizedGravityZ = (float) (vector[2] / vectorMagnitude);
+
+ final int inclination = (int) Math.round(Math.toDegrees(Math.acos(normalizedGravityZ)));
+ return inclination < FLAT_INCLINATION_THRESHOLD_DEGREES;
+ }
+
+ private Integer getOrientationSensorRotation() {
+ if (mLastDeviceOrientationSensorEvent == null) return null;
+ return (int) mLastDeviceOrientationSensorEvent.values[0];
+ }
+
+ /**
+ * Called whenever display status changes, we use this signal to start/stop listening
+ * to sensors when the display is off to save battery. Using display state instead of
+ * general power state to reduce the time when sensors are on, we don't need to listen
+ * to the extra sensors when the screen is off.
+ */
+ public void onDisplayPowerStatusChanged(boolean screenTurnedOn) {
+ mScreenTurnedOn = screenTurnedOn;
+ mConditionedSensorListener.updateListeningState();
+ }
+
+ /**
+ * Called whenever we display rotation might have been updated
+ * @param rotation new rotation
+ */
+ public void onDisplayRotationChanged(int rotation) {
+ mLastScreenRotation = rotation;
+ }
+
+ /**
+ * Called whenever foldable device becomes fully closed or opened
+ */
+ public void onDeviceClosedStatusChanged(boolean deviceClosed) {
+ mDeviceClosed = deviceClosed;
+ mConditionedSensorListener.updateListeningState();
+ }
+ }
+
+ /**
+ * Helper class that subscribes or unsubscribes from a sensor based on a condition specified
+ * in {@link SensorSubscription}
+ */
+ static class ConditionSensorListener {
+ private final List<SensorSubscription> mSensorSubscriptions;
+ private final ArraySet<Sensor> mIsListening = new ArraySet<>();
+
+ private final SensorManager mSensorManager;
+ private final SensorEventListener mSensorEventListener;
+
+ private final Handler mHandler;
+
+ public ConditionSensorListener(SensorManager sensorManager,
+ SensorEventListener sensorEventListener, Handler handler,
+ List<SensorSubscription> sensorSubscriptions) {
+ mSensorManager = sensorManager;
+ mSensorEventListener = sensorEventListener;
+ mSensorSubscriptions = sensorSubscriptions;
+ mHandler = handler;
+ }
+
+ /**
+ * Updates current listening state of the sensor based on the provided conditions
+ */
+ public void updateListeningState() {
+ for (int i = 0; i < mSensorSubscriptions.size(); i++) {
+ final SensorSubscription subscription = mSensorSubscriptions.get(i);
+ final Sensor sensor = subscription.mSensor;
+
+ final boolean shouldBeListening = subscription.mAllowedToListenSupplier.get();
+ final boolean isListening = mIsListening.contains(sensor);
+ final boolean shouldUpdateListening = isListening != shouldBeListening;
+
+ if (shouldUpdateListening) {
+ if (shouldBeListening) {
+ mIsListening.add(sensor);
+ mSensorManager.registerListener(mSensorEventListener, sensor,
+ SENSOR_DELAY_NORMAL, mHandler);
+ } else {
+ mIsListening.remove(sensor);
+ mSensorManager.unregisterListener(mSensorEventListener, sensor);
+ subscription.mOnUnsubscribe.run();
+ }
+ }
+ }
+ }
+
+ /**
+ * Represents a configuration of a single sensor subscription
+ */
+ public static class SensorSubscription {
+ private final Sensor mSensor;
+ private final Supplier<Boolean> mAllowedToListenSupplier;
+ private final Runnable mOnUnsubscribe;
+
+ /**
+ * @param sensor sensor to listen to
+ * @param allowedToListen return true when it is allowed to listen to the sensor
+ * @param cleanup a runnable that will be closed just before unsubscribing from the
+ * sensor
+ */
+
+ public SensorSubscription(Sensor sensor, Supplier<Boolean> allowedToListen,
+ Runnable cleanup) {
+ mSensor = sensor;
+ mAllowedToListenSupplier = allowedToListen;
+ mOnUnsubscribe = cleanup;
+ }
+ }
+ }
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
similarity index 73%
rename from services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
rename to services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
index 5968b63..ad938af 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
@@ -21,10 +21,12 @@
import static com.android.server.devicestate.DeviceState.FLAG_EMULATED_ONLY;
import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE;
import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL;
+import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS;
import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig;
import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createTentModeClosedState;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
@@ -39,12 +41,15 @@
import java.util.function.Predicate;
/**
- * Device state policy for a foldable device that supports tent mode: a mode when the device
- * keeps the outer display on until reaching a certain hinge angle threshold.
+ * Device state policy for a foldable device with two screens in a book style, where the hinge is
+ * located on the left side of the device when in folded posture.
+ * The policy supports tent/wedge mode: a mode when the device keeps the outer display on
+ * until reaching certain conditions like hinge angle threshold.
*
* Contains configuration for {@link FoldableDeviceStateProvider}.
*/
-public class TentModeDeviceStatePolicy extends DeviceStatePolicy {
+public class BookStyleDeviceStatePolicy extends DeviceStatePolicy implements
+ BookStyleClosedStatePredicate.ClosedStateUpdatesListener {
private static final int DEVICE_STATE_CLOSED = 0;
private static final int DEVICE_STATE_HALF_OPENED = 1;
@@ -57,9 +62,10 @@
private static final int MIN_CLOSED_ANGLE_DEGREES = 0;
private static final int MAX_CLOSED_ANGLE_DEGREES = 5;
- private final DeviceStateProvider mProvider;
+ private final FoldableDeviceStateProvider mProvider;
private final boolean mIsDualDisplayBlockingEnabled;
+ private final boolean mEnablePostureBasedClosedState;
private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true;
private static final Predicate<FoldableDeviceStateProvider> NOT_ALLOWED = p -> false;
@@ -73,30 +79,30 @@
* between folded and unfolded modes, otherwise when folding the
* display switch will happen at 0 degrees
*/
- public TentModeDeviceStatePolicy(@NonNull Context context,
- @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, int closeAngleDegrees) {
- this(new FeatureFlagsImpl(), context, hingeAngleSensor, hallSensor, closeAngleDegrees);
- }
-
- public TentModeDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context,
- @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor,
- int closeAngleDegrees) {
+ public BookStyleDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context,
+ @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor,
+ @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+ Integer closeAngleDegrees) {
super(context);
final SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
- final DeviceStateConfiguration[] configuration = createConfiguration(closeAngleDegrees);
-
+ mEnablePostureBasedClosedState = featureFlags.enableFoldablesPostureBasedClosedState();
mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
+ final DeviceStateConfiguration[] configuration = createConfiguration(
+ leftAccelerometerSensor, rightAccelerometerSensor, closeAngleDegrees);
+
mProvider = new FoldableDeviceStateProvider(mContext, sensorManager,
hingeAngleSensor, hallSensor, displayManager, configuration);
}
- private DeviceStateConfiguration[] createConfiguration(int closeAngleDegrees) {
+ private DeviceStateConfiguration[] createConfiguration(@Nullable Sensor leftAccelerometerSensor,
+ @Nullable Sensor rightAccelerometerSensor, Integer closeAngleDegrees) {
return new DeviceStateConfiguration[]{
- createClosedConfiguration(closeAngleDegrees),
+ createClosedConfiguration(leftAccelerometerSensor, rightAccelerometerSensor,
+ closeAngleDegrees),
createConfig(DEVICE_STATE_HALF_OPENED,
/* name= */ "HALF_OPENED",
/* activeStatePredicate= */ (provider) -> {
@@ -123,8 +129,10 @@
};
}
- private DeviceStateConfiguration createClosedConfiguration(int closeAngleDegrees) {
- if (closeAngleDegrees > 0) {
+ private DeviceStateConfiguration createClosedConfiguration(
+ @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+ @Nullable Integer closeAngleDegrees) {
+ if (closeAngleDegrees != null) {
// Switch displays at closeAngleDegrees in both ways (folding and unfolding)
return createConfig(
DEVICE_STATE_CLOSED,
@@ -137,6 +145,19 @@
);
}
+ if (mEnablePostureBasedClosedState) {
+ // Use smart closed state predicate that will use different switch angles
+ // based on the device posture (e.g. wedge mode, tent mode, reverse wedge mode)
+ return createConfig(
+ DEVICE_STATE_CLOSED,
+ /* name= */ "CLOSED",
+ /* flags= */ FLAG_CANCEL_OVERRIDE_REQUESTS,
+ /* activeStatePredicate= */ new BookStyleClosedStatePredicate(mContext,
+ this, leftAccelerometerSensor, rightAccelerometerSensor,
+ DEFAULT_STATE_TRANSITIONS)
+ );
+ }
+
// Switch to the outer display only at 0 degrees but use TENT_MODE_SWITCH_ANGLE_DEGREES
// angle when switching to the inner display
return createTentModeClosedState(DEVICE_STATE_CLOSED,
@@ -148,6 +169,11 @@
}
@Override
+ public void onClosedStateUpdated() {
+ mProvider.notifyDeviceStateChangedIfNeeded();
+ }
+
+ @Override
public DeviceStateProvider getDeviceStateProvider() {
return mProvider;
}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java
new file mode 100644
index 0000000..8977422
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2023 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.policy;
+
+import android.annotation.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Calculates if we should use outer or inner display on foldable devices based on a several
+ * inputs like device orientation, hinge angle signals.
+ *
+ * This is a stateful class and acts like a state machine with fixed number of states
+ * and transitions. It allows to list all possible state transitions instead of performing
+ * imperative logic to make sure that we cover all scenarios and improve debuggability.
+ *
+ * See {@link BookStyleStateTransitions} for detailed description of the default behavior.
+ */
+public class BookStylePreferredScreenCalculator {
+
+ /**
+ * When calculating the new state we will re-calculate it until it settles down. We re-calculate
+ * it because the new state might trigger another state transition and this might happen
+ * several times. We don't want to have infinite loops in state calculation, so this value
+ * limits the number of such state transitions.
+ * For example, in the default configuration {@link BookStyleStateTransitions}, after each
+ * transition with 'set sticky flag' output it will perform a transition to a state without
+ * 'set sticky flag' output.
+ * We also have a unit test covering all possible states which checks that we don't have such
+ * states that could end up in an infinite transition. See sample test for the default
+ * transitions in {@link BookStyleClosedStateCalculatorTest}.
+ */
+ private static final int MAX_STATE_CHANGES = 16;
+
+ private State mState = new State(
+ /* stickyKeepOuterUntil90Degrees= */ false,
+ /* stickyKeepInnerUntil45Degrees= */ false,
+ PreferredScreen.INVALID);
+
+ private final List<StateTransition> mStateTransitions;
+
+ /**
+ * Creates BookStyleClosedStateCalculator
+ * @param stateTransitions list of all state transitions
+ */
+ public BookStylePreferredScreenCalculator(List<StateTransition> stateTransitions) {
+ mStateTransitions = stateTransitions;
+ }
+
+ /**
+ * Calculates updated {@link PreferredScreen} based on the current inputs and the current state.
+ * The calculation is done based on defined {@link StateTransition}s, it might perform
+ * multiple transitions until we settle down on a single state. Multiple transitions could be
+ * performed in case if {@link StateTransition} causes another update of the state.
+ * There is a limit of maximum {@link MAX_STATE_CHANGES} state transitions, after which
+ * this method will throw an {@link IllegalStateException}.
+ *
+ * @param angle current hinge angle
+ * @param likelyTentOrWedge true if the device is likely in tent or wedge mode
+ * @param likelyReverseWedge true if the device is likely in reverse wedge mode
+ * @return updated {@link PreferredScreen}
+ */
+ public PreferredScreen calculatePreferredScreen(HingeAngle angle, boolean likelyTentOrWedge,
+ boolean likelyReverseWedge) {
+ int attempts = 0;
+ State newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge);
+ while (attempts < MAX_STATE_CHANGES && !Objects.equals(mState, newState)) {
+ mState = newState;
+ newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge);
+ attempts++;
+ }
+
+ if (attempts >= MAX_STATE_CHANGES) {
+ throw new IllegalStateException(
+ "Can't settle state " + mState + ", inputs: hingeAngle = " + angle
+ + ", likelyTentOrWedge = " + likelyTentOrWedge
+ + ", likelyReverseWedge = " + likelyReverseWedge);
+ }
+
+ final State oldState = mState;
+ mState = newState;
+
+ if (mState.mPreferredScreen == PreferredScreen.INVALID) {
+ throw new IllegalStateException(
+ "Reached invalid state " + mState + ", inputs: hingeAngle = " + angle
+ + ", likelyTentOrWedge = " + likelyTentOrWedge
+ + ", likelyReverseWedge = " + likelyReverseWedge + ", old state: "
+ + oldState);
+ }
+
+ return mState.mPreferredScreen;
+ }
+
+ /**
+ * Returns the current state of the calculator
+ */
+ public State getState() {
+ return mState;
+ }
+
+ private State calculateNewState(State current, HingeAngle hingeAngle, boolean likelyTentOrWedge,
+ boolean likelyReverseWedge) {
+ for (int i = 0; i < mStateTransitions.size(); i++) {
+ final State newState = mStateTransitions.get(i).tryTransition(hingeAngle,
+ likelyTentOrWedge, likelyReverseWedge, current);
+ if (newState != null) {
+ return newState;
+ }
+ }
+
+ throw new IllegalArgumentException(
+ "Entry not found for state: " + current + ", hingeAngle = " + hingeAngle
+ + ", likelyTentOrWedge = " + likelyTentOrWedge + ", likelyReverseWedge = "
+ + likelyReverseWedge);
+ }
+
+ /**
+ * The angle between two halves of the foldable device in degrees. The angle is '0' when
+ * the device is fully closed and '180' when the device is fully open and flat.
+ */
+ public enum HingeAngle {
+ ANGLE_0,
+ ANGLE_0_TO_45,
+ ANGLE_45_TO_90,
+ ANGLE_90_TO_180
+ }
+
+ /**
+ * Resulting closed state of the device, where OPEN state indicates that the device should use
+ * the inner display and CLOSED means that it should use the outer (cover) screen.
+ */
+ public enum PreferredScreen {
+ INNER,
+ OUTER,
+ INVALID
+ }
+
+ /**
+ * Describes a state transition for the posture based active screen calculator
+ */
+ public static class StateTransition {
+ private final Input mInput;
+ private final State mOutput;
+
+ public StateTransition(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+ boolean likelyReverseWedge,
+ boolean stickyKeepOuterUntil90Degrees, boolean stickyKeepInnerUntil45Degrees,
+ PreferredScreen preferredScreen, Boolean setStickyKeepOuterUntil90Degrees,
+ Boolean setStickyKeepInnerUntil45Degrees) {
+ mInput = new Input(hingeAngle, likelyTentOrWedge, likelyReverseWedge,
+ stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees);
+ mOutput = new State(setStickyKeepOuterUntil90Degrees,
+ setStickyKeepInnerUntil45Degrees, preferredScreen);
+ }
+
+ /**
+ * Returns true if the state transition is applicable for the given inputs
+ */
+ private boolean isApplicable(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+ boolean likelyReverseWedge, State currentState) {
+ return mInput.hingeAngle == hingeAngle
+ && mInput.likelyTentOrWedge == likelyTentOrWedge
+ && mInput.likelyReverseWedge == likelyReverseWedge
+ && Objects.equals(mInput.stickyKeepOuterUntil90Degrees,
+ currentState.stickyKeepOuterUntil90Degrees)
+ && Objects.equals(mInput.stickyKeepInnerUntil45Degrees,
+ currentState.stickyKeepInnerUntil45Degrees);
+ }
+
+ /**
+ * Try to perform transition for the inputs, returns new state if this
+ * transition is applicable for the given state and inputs
+ */
+ @Nullable
+ State tryTransition(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+ boolean likelyReverseWedge, State currentState) {
+ if (!isApplicable(hingeAngle, likelyTentOrWedge, likelyReverseWedge, currentState)) {
+ return null;
+ }
+
+ boolean stickyKeepOuterUntil90Degrees = currentState.stickyKeepOuterUntil90Degrees;
+ boolean stickyKeepInnerUntil45Degrees = currentState.stickyKeepInnerUntil45Degrees;
+
+ if (mOutput.stickyKeepOuterUntil90Degrees != null) {
+ stickyKeepOuterUntil90Degrees =
+ mOutput.stickyKeepOuterUntil90Degrees;
+ }
+
+ if (mOutput.stickyKeepInnerUntil45Degrees != null) {
+ stickyKeepInnerUntil45Degrees =
+ mOutput.stickyKeepInnerUntil45Degrees;
+ }
+
+ return new State(stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees,
+ mOutput.mPreferredScreen);
+ }
+ }
+
+ /**
+ * The input part of the {@link StateTransition}, these are the values that are used
+ * to decide which {@link State} output to choose.
+ */
+ private static class Input {
+ final HingeAngle hingeAngle;
+ final boolean likelyTentOrWedge;
+ final boolean likelyReverseWedge;
+ final boolean stickyKeepOuterUntil90Degrees;
+ final boolean stickyKeepInnerUntil45Degrees;
+
+ public Input(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+ boolean likelyReverseWedge,
+ boolean stickyKeepOuterUntil90Degrees, boolean stickyKeepInnerUntil45Degrees) {
+ this.hingeAngle = hingeAngle;
+ this.likelyTentOrWedge = likelyTentOrWedge;
+ this.likelyReverseWedge = likelyReverseWedge;
+ this.stickyKeepOuterUntil90Degrees = stickyKeepOuterUntil90Degrees;
+ this.stickyKeepInnerUntil45Degrees = stickyKeepInnerUntil45Degrees;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Input)) return false;
+ Input that = (Input) o;
+ return likelyTentOrWedge == that.likelyTentOrWedge
+ && likelyReverseWedge == that.likelyReverseWedge
+ && stickyKeepOuterUntil90Degrees == that.stickyKeepOuterUntil90Degrees
+ && stickyKeepInnerUntil45Degrees == that.stickyKeepInnerUntil45Degrees
+ && hingeAngle == that.hingeAngle;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(hingeAngle, likelyTentOrWedge, likelyReverseWedge,
+ stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees);
+ }
+
+ @Override
+ public String toString() {
+ return "InputState{" +
+ "hingeAngle=" + hingeAngle +
+ ", likelyTentOrWedge=" + likelyTentOrWedge +
+ ", likelyReverseWedge=" + likelyReverseWedge +
+ ", stickyKeepOuterUntil90Degrees=" + stickyKeepOuterUntil90Degrees +
+ ", stickyKeepInnerUntil45Degrees=" + stickyKeepInnerUntil45Degrees +
+ '}';
+ }
+ }
+
+ /**
+ * Class that holds a state of the calculator, it could be used to store the current
+ * state or to define the target (output) state based on some input in {@link StateTransition}.
+ */
+ public static class State {
+ public Boolean stickyKeepOuterUntil90Degrees;
+ public Boolean stickyKeepInnerUntil45Degrees;
+
+ PreferredScreen mPreferredScreen;
+
+ public State(Boolean stickyKeepOuterUntil90Degrees,
+ Boolean stickyKeepInnerUntil45Degrees,
+ PreferredScreen preferredScreen) {
+ this.stickyKeepOuterUntil90Degrees = stickyKeepOuterUntil90Degrees;
+ this.stickyKeepInnerUntil45Degrees = stickyKeepInnerUntil45Degrees;
+ this.mPreferredScreen = preferredScreen;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof State)) return false;
+ State that = (State) o;
+ return Objects.equals(stickyKeepOuterUntil90Degrees,
+ that.stickyKeepOuterUntil90Degrees) && Objects.equals(
+ stickyKeepInnerUntil45Degrees, that.stickyKeepInnerUntil45Degrees)
+ && mPreferredScreen == that.mPreferredScreen;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees,
+ mPreferredScreen);
+ }
+
+ @Override
+ public String toString() {
+ return "State{" +
+ "stickyKeepOuterUntil90Degrees=" + stickyKeepOuterUntil90Degrees +
+ ", stickyKeepInnerUntil90Degrees=" + stickyKeepInnerUntil45Degrees +
+ ", closedState=" + mPreferredScreen +
+ '}';
+ }
+ }
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
new file mode 100644
index 0000000..16daacb
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
@@ -0,0 +1,722 @@
+/*
+ * Copyright (C) 2023 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.policy;
+
+import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen;
+import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle;
+import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Describes all possible state transitions for {@link BookStylePreferredScreenCalculator}.
+ * It contains a default configuration for a foldable device that has two screens: smaller outer
+ * screen which has portrait natural orientation and a larger inner screen and allows to use the
+ * device in tent mode or wedge mode.
+ *
+ * As the output state could affect calculating of the new state, it could potentially cause
+ * infinite loop and make the state never settle down. This could be avoided using automated test
+ * that checks all possible inputs and asserts that the final state is valid.
+ * See sample test for the default transitions in {@link BookStyleClosedStateCalculatorTest}.
+ *
+ * - Tent mode is defined as a posture when the device is partially opened and placed on the ground
+ * on the edges that are parallel to the hinge.
+ * - Wedge mode is when the device is partially opened and placed flat on the ground with the part
+ * of the device that doesn't have the display
+ * - Reverse wedge mode is when the device is partially opened and placed flat on the ground with
+ * the outer screen down, so the outer screen is not accessible
+ *
+ * Behavior description:
+ * - When unfolding with screens off we assume that no sensor data available except hinge angle
+ * (based on hall sensor), so we switch to the inner screen immediately
+ *
+ * - When unfolding when screen is 'on' we can check if we are likely in tent or wedge mode
+ * - If not likely tent/wedge mode or sensors data not available, then we unfold immediately
+ * After unfolding, the state of the inner screen 'on' is sticky between 0 and 45 degrees, so
+ * it won't jump back to the outer screen even if you move the phone into tent/wedge mode. The
+ * stickiness is reset after fully closing the device or unfolding past 45 degrees.
+ * - If likely tent or wedge mode, switch only at 90 degrees
+ * Tent/wedge mode is 'sticky' between 0 and 90 degrees, so it won't reset until you either
+ * fully close the device or unfold past 90 degrees.
+ *
+ * - When folding we can check if we are likely in reverse wedge mode
+ * - If not likely in reverse wedge mode or sensor data is not available we switch to the outer
+ * screen at 45 degrees and enable sticky tent/wedge mode as before, this allows to enter
+ * tent/wedge mode even if you are not on an even surface or holding phone in landscape
+ * - If likely in reverse wedge mode, switch to the outer screen only at 0 degrees to allow
+ * some use cases like using camera in this posture, the check happens after passing 45 degrees
+ * and inner screen becomes sticky turned 'on' until fully closing or unfolding past 45 degrees
+ */
+public class BookStyleStateTransitions {
+
+ public static final List<StateTransition> DEFAULT_STATE_TRANSITIONS = new ArrayList<>();
+
+ static {
+ // region Angle 0
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ true
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ false,
+ /* setStickyKeepInnerUntil45Degrees */ true
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ true
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ false,
+ /* setStickyKeepInnerUntil45Degrees */ true
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ false
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ false,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ false
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ // endregion
+
+ // region Angle 0-45
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ true,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ true
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ true,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ true
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_0_TO_45,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ // endregion
+
+ // region Angle 45-90
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ false
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ true
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ false
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ true
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.OUTER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_45_TO_90,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ // endregion
+
+ // region Angle 90-180
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ false
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ false,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ false
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ false,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ false,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ false
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ false,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ false,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ false,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ false
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ false,
+ PreferredScreen.INNER,
+ /* setStickyKeepOuterUntil90Degrees */ false,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+ HingeAngle.ANGLE_90_TO_180,
+ /* likelyTentOrWedge */ true,
+ /* likelyReverseWedge */ true,
+ /* stickyKeepOuterUntil90Degrees */ true,
+ /* stickyKeepInnerUntil45Degrees */ true,
+ PreferredScreen.INVALID,
+ /* setStickyKeepOuterUntil90Degrees */ null,
+ /* setStickyKeepInnerUntil45Degrees */ null
+ ));
+ // endregion
+ }
+}
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
new file mode 100644
index 0000000..8d01b7a
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
@@ -0,0 +1,703 @@
+/*
+ * Copyright (C) 2023 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.policy;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.STATE_OFF;
+import static android.view.Display.STATE_ON;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Instrumentation;
+import android.content.res.Configuration;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.input.InputSensorInfo;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableContext;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.devicestate.DeviceStateProvider;
+import com.android.server.devicestate.DeviceStateProvider.Listener;
+import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl;
+import com.android.server.policy.feature.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.internal.util.reflection.FieldSetter;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link BookStyleDeviceStatePolicy.Provider}.
+ * <p/>
+ * Run with <code>atest BookStyleDeviceStatePolicyTest</code>.
+ */
+@RunWith(AndroidTestingRunner.class)
+public final class BookStyleDeviceStatePolicyTest {
+
+ private static final int DEVICE_STATE_CLOSED = 0;
+ private static final int DEVICE_STATE_HALF_OPENED = 1;
+ private static final int DEVICE_STATE_OPENED = 2;
+
+ @Captor
+ private ArgumentCaptor<Integer> mDeviceStateCaptor;
+ @Captor
+ private ArgumentCaptor<DisplayManager.DisplayListener> mDisplayListenerCaptor;
+ @Mock
+ private SensorManager mSensorManager;
+ @Mock
+ private InputSensorInfo mInputSensorInfo;
+ @Mock
+ private Listener mListener;
+ @Mock
+ DisplayManager mDisplayManager;
+ @Mock
+ private Display mDisplay;
+
+ private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl();
+
+ private final Configuration mConfiguration = new Configuration();
+
+ private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
+ @Rule
+ public final TestableContext mContext = new TestableContext(
+ mInstrumentation.getTargetContext());
+
+ private Sensor mHallSensor;
+ private Sensor mOrientationSensor;
+ private Sensor mHingeAngleSensor;
+ private Sensor mLeftAccelerometer;
+ private Sensor mRightAccelerometer;
+
+ private Map<Sensor, List<SensorEventListener>> mSensorEventListeners = new HashMap<>();
+ private DeviceStateProvider mProvider;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_FOLDABLES_POSTURE_BASED_CLOSED_STATE, true);
+ mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_DUAL_DISPLAY_BLOCKING, true);
+
+ when(mInputSensorInfo.getName()).thenReturn("hall-effect");
+ mHallSensor = new Sensor(mInputSensorInfo);
+ when(mInputSensorInfo.getName()).thenReturn("hinge-angle");
+ mHingeAngleSensor = new Sensor(mInputSensorInfo);
+ when(mInputSensorInfo.getName()).thenReturn("left-accelerometer");
+ mLeftAccelerometer = new Sensor(mInputSensorInfo);
+ when(mInputSensorInfo.getName()).thenReturn("right-accelerometer");
+ mRightAccelerometer = new Sensor(mInputSensorInfo);
+ when(mInputSensorInfo.getName()).thenReturn("orientation");
+ mOrientationSensor = new Sensor(mInputSensorInfo);
+
+ mContext.addMockSystemService(SensorManager.class, mSensorManager);
+
+ when(mSensorManager.getDefaultSensor(eq(Sensor.TYPE_HINGE_ANGLE), eq(true)))
+ .thenReturn(mHingeAngleSensor);
+ when(mSensorManager.getDefaultSensor(eq(Sensor.TYPE_DEVICE_ORIENTATION)))
+ .thenReturn(mOrientationSensor);
+
+ when(mDisplayManager.getDisplay(eq(DEFAULT_DISPLAY))).thenReturn(mDisplay);
+ mContext.addMockSystemService(DisplayManager.class, mDisplayManager);
+
+ mContext.ensureTestableResources();
+ when(mContext.getResources().getConfiguration()).thenReturn(mConfiguration);
+
+ final List<Sensor> sensors = new ArrayList<>();
+ sensors.add(mHallSensor);
+ sensors.add(mHingeAngleSensor);
+ sensors.add(mOrientationSensor);
+ sensors.add(mLeftAccelerometer);
+ sensors.add(mRightAccelerometer);
+
+ when(mSensorManager.registerListener(any(), any(), anyInt(), any())).thenAnswer(
+ invocation -> {
+ final SensorEventListener listener = invocation.getArgument(0);
+ final Sensor sensor = invocation.getArgument(1);
+ addSensorListener(sensor, listener);
+ return true;
+ });
+ when(mSensorManager.registerListener(any(), any(), anyInt())).thenAnswer(
+ invocation -> {
+ final SensorEventListener listener = invocation.getArgument(0);
+ final Sensor sensor = invocation.getArgument(1);
+ addSensorListener(sensor, listener);
+ return true;
+ });
+
+ doAnswer(invocation -> {
+ final SensorEventListener listener = invocation.getArgument(0);
+ final boolean[] removed = {false};
+ mSensorEventListeners.forEach((sensor, sensorEventListeners) ->
+ removed[0] |= sensorEventListeners.remove(listener));
+
+ if (!removed[0]) {
+ throw new IllegalArgumentException(
+ "Trying to unregister listener " + listener + " that was not registered");
+ }
+
+ return null;
+ }).when(mSensorManager).unregisterListener(any(SensorEventListener.class));
+
+ doAnswer(invocation -> {
+ final SensorEventListener listener = invocation.getArgument(0);
+ final Sensor sensor = invocation.getArgument(1);
+
+ boolean removed = mSensorEventListeners.get(sensor).remove(listener);
+ if (!removed) {
+ throw new IllegalArgumentException(
+ "Trying to unregister listener " + listener
+ + " that was not registered for sensor " + sensor);
+ }
+
+ return null;
+ }).when(mSensorManager).unregisterListener(any(SensorEventListener.class),
+ any(Sensor.class));
+
+ try {
+ FieldSetter.setField(mHallSensor, mHallSensor.getClass()
+ .getDeclaredField("mStringType"), "com.google.sensor.hall_effect");
+ } catch (NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+
+ when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL)))
+ .thenReturn(sensors);
+
+ mInstrumentation.runOnMainSync(() -> mProvider = createProvider());
+
+ verify(mDisplayManager, atLeastOnce()).registerDisplayListener(
+ mDisplayListenerCaptor.capture(), nullable(Handler.class));
+ setScreenOn(true);
+ }
+
+ @Test
+ public void test_noSensorEventsYet_reportOpenedState() {
+ mProvider.setListener(mListener);
+ verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+ assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue());
+ }
+
+ @Test
+ public void test_deviceClosedSensorEventsBecameAvailable_reportsClosedState() {
+ mProvider.setListener(mListener);
+ clearInvocations(mListener);
+
+ sendHingeAngle(0f);
+
+ verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+ assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue());
+ }
+
+ @Test
+ public void test_hingeAngleClosed_reportsClosedState() {
+ sendHingeAngle(0f);
+
+ mProvider.setListener(mListener);
+ verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+ assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue());
+ }
+
+ @Test
+ public void test_hingeAngleFullyOpened_reportsOpenedState() {
+ sendHingeAngle(180f);
+
+ mProvider.setListener(mListener);
+ verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+ assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue());
+ }
+
+ @Test
+ public void test_unfoldingFromClosedToFullyOpened_reportsOpenedEvent() {
+ sendHingeAngle(0f);
+ mProvider.setListener(mListener);
+ clearInvocations(mListener);
+
+ sendHingeAngle(180f);
+
+ verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+ assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue());
+ }
+
+ @Test
+ public void test_foldingFromFullyOpenToFullyClosed_movesToClosedState() {
+ sendHingeAngle(180f);
+
+ sendHingeAngle(0f);
+
+ mProvider.setListener(mListener);
+ verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+ assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue());
+ }
+
+ @Test
+ public void test_slowUnfolding_reportsEventsInOrder() {
+ sendHingeAngle(0f);
+ mProvider.setListener(mListener);
+
+ sendHingeAngle(5f);
+ sendHingeAngle(10f);
+ sendHingeAngle(60f);
+ sendHingeAngle(100f);
+ sendHingeAngle(180f);
+
+ verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+ assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+ DEVICE_STATE_CLOSED,
+ DEVICE_STATE_HALF_OPENED,
+ DEVICE_STATE_OPENED
+ );
+ }
+
+ @Test
+ public void test_slowFolding_reportsEventsInOrder() {
+ sendHingeAngle(180f);
+ mProvider.setListener(mListener);
+
+ sendHingeAngle(180f);
+ sendHingeAngle(100f);
+ sendHingeAngle(60f);
+ sendHingeAngle(10f);
+ sendHingeAngle(5f);
+
+ verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+ assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+ DEVICE_STATE_OPENED,
+ DEVICE_STATE_HALF_OPENED,
+ DEVICE_STATE_CLOSED
+ );
+ }
+
+ @Test
+ public void test_hingeAngleOpen_screenOff_reportsHalfFolded() {
+ sendHingeAngle(0f);
+ setScreenOn(false);
+ mProvider.setListener(mListener);
+
+ sendHingeAngle(10f);
+
+ verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+ assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+ DEVICE_STATE_CLOSED,
+ DEVICE_STATE_HALF_OPENED
+ );
+ }
+
+ @Test
+ public void test_slowUnfoldingWithScreenOff_reportsEventsInOrder() {
+ sendHingeAngle(0f);
+ setScreenOn(false);
+ mProvider.setListener(mListener);
+
+ sendHingeAngle(5f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ sendHingeAngle(10f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ sendHingeAngle(60f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ sendHingeAngle(100f);
+ sendHingeAngle(180f);
+ assertLatestReportedState(DEVICE_STATE_OPENED);
+
+ verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+ assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+ DEVICE_STATE_CLOSED,
+ DEVICE_STATE_HALF_OPENED,
+ DEVICE_STATE_OPENED
+ );
+ }
+
+ @Test
+ public void test_unfoldWithScreenOff_reportsHalfOpened() {
+ sendHingeAngle(0f);
+ setScreenOn(false);
+ mProvider.setListener(mListener);
+
+ sendHingeAngle(5f);
+ sendHingeAngle(10f);
+
+ verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+ assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+ DEVICE_STATE_CLOSED,
+ DEVICE_STATE_HALF_OPENED
+ );
+ }
+
+ @Test
+ public void test_slowUnfoldingAndFolding_reportsEventsInOrder() {
+ sendHingeAngle(0f);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+ // Started unfolding
+ sendHingeAngle(5f);
+ sendHingeAngle(30f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ sendHingeAngle(60f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ sendHingeAngle(100f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ sendHingeAngle(180f);
+ assertLatestReportedState(DEVICE_STATE_OPENED);
+
+ // Started folding
+ sendHingeAngle(100f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ sendHingeAngle(60f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ sendHingeAngle(30f);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ sendHingeAngle(5f);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+ verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+ assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+ DEVICE_STATE_CLOSED,
+ DEVICE_STATE_HALF_OPENED,
+ DEVICE_STATE_OPENED,
+ DEVICE_STATE_HALF_OPENED,
+ DEVICE_STATE_CLOSED
+ );
+ }
+
+ @Test
+ public void test_unfoldTo30Degrees_screenOnRightSideMostlyFlat_keepsClosedState() {
+ sendHingeAngle(0f);
+ sendRightSideFlatSensorEvent(true);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ clearInvocations(mListener);
+
+ sendHingeAngle(30f);
+
+ verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+ }
+
+ @Test
+ public void test_unfoldTo30Degrees_seascapeDeviceOrientation_keepsClosedState() {
+ sendHingeAngle(0f);
+ sendRightSideFlatSensorEvent(false);
+ sendDeviceOrientation(Surface.ROTATION_270);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ clearInvocations(mListener);
+
+ sendHingeAngle(30f);
+
+ verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+ }
+
+ @Test
+ public void test_unfoldTo30Degrees_landscapeScreenRotation_keepsClosedState() {
+ sendHingeAngle(0f);
+ sendRightSideFlatSensorEvent(false);
+ sendScreenRotation(Surface.ROTATION_90);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ clearInvocations(mListener);
+
+ sendHingeAngle(30f);
+
+ verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+ }
+
+ @Test
+ public void test_unfoldTo30Degrees_seascapeScreenRotation_keepsClosedState() {
+ sendHingeAngle(0f);
+ sendRightSideFlatSensorEvent(false);
+ sendScreenRotation(Surface.ROTATION_270);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ clearInvocations(mListener);
+
+ sendHingeAngle(30f);
+
+ verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+ }
+
+ @Test
+ public void test_unfoldTo30Degrees_screenOnRightSideNotFlat_switchesToHalfOpenState() {
+ sendHingeAngle(0f);
+ sendRightSideFlatSensorEvent(false);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ clearInvocations(mListener);
+
+ sendHingeAngle(30f);
+
+ verify(mListener).onStateChanged(DEVICE_STATE_HALF_OPENED);
+ }
+
+ @Test
+ public void test_unfoldTo30Degrees_screenOffRightSideFlat_switchesToHalfOpenState() {
+ sendHingeAngle(0f);
+ setScreenOn(false);
+ // This sensor event should be ignored as screen is off
+ sendRightSideFlatSensorEvent(true);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ clearInvocations(mListener);
+
+ sendHingeAngle(30f);
+
+ verify(mListener).onStateChanged(DEVICE_STATE_HALF_OPENED);
+ }
+
+ @Test
+ public void test_unfoldTo60Degrees_andFoldTo10_switchesToClosedState() {
+ sendHingeAngle(0f);
+ sendRightSideFlatSensorEvent(false);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ sendHingeAngle(60f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ clearInvocations(mListener);
+
+ sendHingeAngle(10f);
+
+ verify(mListener).onStateChanged(DEVICE_STATE_CLOSED);
+ }
+
+ @Test
+ public void test_foldTo10AndUnfoldTo85Degrees_keepsClosedState() {
+ sendHingeAngle(0f);
+ sendRightSideFlatSensorEvent(false);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ sendHingeAngle(180f);
+ assertLatestReportedState(DEVICE_STATE_OPENED);
+ sendHingeAngle(10f);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+ sendHingeAngle(85f);
+
+ // Keeps 'tent'/'wedge' mode even when right side is not flat
+ // as user manually folded the device not all the way
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ }
+
+ @Test
+ public void test_foldTo0AndUnfoldTo85Degrees_doesNotKeepClosedState() {
+ sendHingeAngle(0f);
+ sendRightSideFlatSensorEvent(false);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ sendHingeAngle(180f);
+ assertLatestReportedState(DEVICE_STATE_OPENED);
+ sendHingeAngle(0f);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+ sendHingeAngle(85f);
+
+ // Do not enter 'tent'/'wedge' mode when right side is not flat
+ // as user fully folded the device before that
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ }
+
+ @Test
+ public void test_foldTo10_leftSideIsFlat_keepsInnerScreenForReverseWedge() {
+ sendHingeAngle(180f);
+ sendLeftSideFlatSensorEvent(true);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_OPENED);
+
+ sendHingeAngle(10f);
+
+ // Keep the inner screen for reverse wedge mode (e.g. for astrophotography use case)
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ }
+
+ @Test
+ public void test_foldTo10_leftSideIsNotFlat_switchesToOuterScreen() {
+ sendHingeAngle(180f);
+ sendLeftSideFlatSensorEvent(false);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_OPENED);
+
+ sendHingeAngle(10f);
+
+ // Do not keep the inner screen as it is not reverse wedge mode
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ }
+
+ @Test
+ public void test_foldTo10_noAccelerometerEvents_switchesToOuterScreen() {
+ sendHingeAngle(180f);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_OPENED);
+
+ sendHingeAngle(10f);
+
+ // Do not keep the inner screen as it is not reverse wedge mode
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ }
+
+ @Test
+ public void test_deviceClosed_screenIsOff_noSensorListeners() {
+ mProvider.setListener(mListener);
+
+ sendHingeAngle(0f);
+ setScreenOn(false);
+
+ assertNoListenersForSensor(mLeftAccelerometer);
+ assertNoListenersForSensor(mRightAccelerometer);
+ assertNoListenersForSensor(mOrientationSensor);
+ }
+
+ @Test
+ public void test_deviceClosed_screenIsOn_doesNotListenForOneAccelerometer() {
+ mProvider.setListener(mListener);
+
+ sendHingeAngle(0f);
+ setScreenOn(true);
+
+ assertNoListenersForSensor(mLeftAccelerometer);
+ assertListensForSensor(mRightAccelerometer);
+ assertListensForSensor(mOrientationSensor);
+ }
+
+ @Test
+ public void test_deviceOpened_screenIsOn_listensToSensors() {
+ mProvider.setListener(mListener);
+
+ sendHingeAngle(180f);
+ setScreenOn(true);
+
+ assertListensForSensor(mLeftAccelerometer);
+ assertListensForSensor(mRightAccelerometer);
+ assertListensForSensor(mOrientationSensor);
+ }
+
+ private void assertLatestReportedState(int state) {
+ final ArgumentCaptor<Integer> integerCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mListener, atLeastOnce()).onStateChanged(integerCaptor.capture());
+ assertEquals(state, integerCaptor.getValue().intValue());
+ }
+
+ private void sendHingeAngle(float angle) {
+ sendSensorEvent(mHingeAngleSensor, new float[]{angle});
+ }
+
+ private void sendDeviceOrientation(int orientation) {
+ sendSensorEvent(mOrientationSensor, new float[]{orientation});
+ }
+
+ private void sendScreenRotation(int rotation) {
+ when(mDisplay.getRotation()).thenReturn(rotation);
+ mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY));
+ }
+
+ private void sendRightSideFlatSensorEvent(boolean flat) {
+ sendAccelerometerFlatEvents(mRightAccelerometer, flat);
+ }
+
+ private void sendLeftSideFlatSensorEvent(boolean flat) {
+ sendAccelerometerFlatEvents(mLeftAccelerometer, flat);
+ }
+
+ private static final int ACCELEROMETER_EVENTS = 10;
+
+ private void sendAccelerometerFlatEvents(Sensor sensor, boolean flat) {
+ final float[] values = flat ? new float[]{0.00021f, -0.00013f, 9.7899f} :
+ new float[]{6.124f, 4.411f, -1.7899f};
+ // Send the same values multiple times to bypass noise filter
+ for (int i = 0; i < ACCELEROMETER_EVENTS; i++) {
+ sendSensorEvent(sensor, values);
+ }
+ }
+
+ private void setScreenOn(boolean isOn) {
+ int state = isOn ? STATE_ON : STATE_OFF;
+ when(mDisplay.getState()).thenReturn(state);
+ mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY));
+ }
+
+ private void sendSensorEvent(Sensor sensor, float[] values) {
+ SensorEvent event = mock(SensorEvent.class);
+ event.sensor = sensor;
+ try {
+ FieldSetter.setField(event, event.getClass().getField("values"),
+ values);
+ } catch (NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+
+ List<SensorEventListener> listeners = mSensorEventListeners.get(sensor);
+ if (listeners != null) {
+ listeners.forEach(sensorEventListener -> sensorEventListener.onSensorChanged(event));
+ }
+ }
+
+ private void assertNoListenersForSensor(Sensor sensor) {
+ final List<SensorEventListener> listeners = mSensorEventListeners.getOrDefault(sensor,
+ new ArrayList<>());
+ assertWithMessage("Expected no listeners for sensor " + sensor + " but found some").that(
+ listeners).isEmpty();
+ }
+
+ private void assertListensForSensor(Sensor sensor) {
+ final List<SensorEventListener> listeners = mSensorEventListeners.getOrDefault(sensor,
+ new ArrayList<>());
+ assertWithMessage(
+ "Expected at least one listener for sensor " + sensor).that(
+ listeners).isNotEmpty();
+ }
+
+ private void addSensorListener(Sensor sensor, SensorEventListener listener) {
+ List<SensorEventListener> listeners = mSensorEventListeners.computeIfAbsent(
+ sensor, k -> new ArrayList<>());
+ listeners.add(listener);
+ }
+
+ private DeviceStateProvider createProvider() {
+ return new BookStyleDeviceStatePolicy(mFakeFeatureFlags, mContext, mHingeAngleSensor,
+ mHallSensor, mLeftAccelerometer, mRightAccelerometer,
+ /* closeAngleDegrees= */ null).getDeviceStateProvider();
+ }
+}
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java
new file mode 100644
index 0000000..ae05b3f
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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.policy;
+
+
+import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.testing.AndroidTestingRunner;
+
+import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen;
+import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle;
+
+import com.google.common.collect.Lists;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link BookStylePreferredScreenCalculator}.
+ * <p/>
+ * Run with <code>atest BookStyleClosedStateCalculatorTest</code>.
+ */
+@RunWith(AndroidTestingRunner.class)
+public final class BookStylePreferredScreenCalculatorTest {
+
+ private final BookStylePreferredScreenCalculator mCalculator =
+ new BookStylePreferredScreenCalculator(DEFAULT_STATE_TRANSITIONS);
+
+ private final List<HingeAngle> mHingeAngleValues = Arrays.asList(HingeAngle.values());
+ private final List<Boolean> mLikelyTentModeValues = Arrays.asList(true, false);
+ private final List<Boolean> mLikelyReverseWedgeModeValues = Arrays.asList(true, false);
+
+ @Test
+ public void transitionAllStates_noCrashes() {
+ final List<List<Object>> arguments = Lists.cartesianProduct(Arrays.asList(
+ mHingeAngleValues,
+ mLikelyTentModeValues,
+ mLikelyReverseWedgeModeValues
+ ));
+
+ arguments.forEach(objects -> {
+ final HingeAngle hingeAngle = (HingeAngle) objects.get(0);
+ final boolean likelyTent = (boolean) objects.get(1);
+ final boolean likelyReverseWedge = (boolean) objects.get(2);
+
+ final String description =
+ "Input: hinge angle = " + hingeAngle + ", likelyTent = " + likelyTent
+ + ", likelyReverseWedge = " + likelyReverseWedge;
+
+ // Verify that there are no crashes because of infinite state transitions and
+ // that it returns a valid active state
+ try {
+ PreferredScreen preferredScreen = mCalculator.calculatePreferredScreen(hingeAngle, likelyTent,
+ likelyReverseWedge);
+
+ assertWithMessage(description).that(preferredScreen).isNotEqualTo(PreferredScreen.INVALID);
+ } catch (Throwable exception) {
+ throw new AssertionError(description, exception);
+ }
+ });
+ }
+}
diff --git a/services/profcollect/Android.bp b/services/profcollect/Android.bp
index 2040bb6..fe431f5 100644
--- a/services/profcollect/Android.bp
+++ b/services/profcollect/Android.bp
@@ -22,24 +22,25 @@
}
filegroup {
- name: "services.profcollect-javasources",
- srcs: ["src/**/*.java"],
- path: "src",
- visibility: ["//frameworks/base/services"],
+ name: "services.profcollect-javasources",
+ srcs: ["src/**/*.java"],
+ path: "src",
+ visibility: ["//frameworks/base/services"],
}
filegroup {
- name: "services.profcollect-sources",
- srcs: [
- ":services.profcollect-javasources",
- ":profcollectd_aidl",
- ],
- visibility: ["//frameworks/base/services:__subpackages__"],
+ name: "services.profcollect-sources",
+ srcs: [
+ ":services.profcollect-javasources",
+ ":profcollectd_aidl",
+ ],
+ visibility: ["//frameworks/base/services:__subpackages__"],
}
java_library_static {
- name: "services.profcollect",
- defaults: ["platform_service_defaults"],
- srcs: [":services.profcollect-sources"],
- libs: ["services.core"],
+ name: "services.profcollect",
+ defaults: ["platform_service_defaults"],
+ srcs: [":services.profcollect-sources"],
+ static_libs: ["services.core"],
+ libs: ["service-art.stubs.system_server"],
}
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index 4007672..582b712 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -41,12 +41,15 @@
import com.android.internal.R;
import com.android.internal.os.BackgroundThread;
import com.android.server.IoThread;
+import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
import com.android.server.SystemService;
+import com.android.server.art.ArtManagerLocal;
import com.android.server.wm.ActivityMetricsLaunchObserver;
import com.android.server.wm.ActivityMetricsLaunchObserverRegistry;
import com.android.server.wm.ActivityTaskManagerInternal;
+import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@@ -261,6 +264,7 @@
BackgroundThread.get().getThreadHandler().post(
() -> {
registerAppLaunchObserver();
+ registerDex2oatObserver();
registerOTAObserver();
});
}
@@ -304,6 +308,44 @@
}
}
+ private void registerDex2oatObserver() {
+ ArtManagerLocal aml = LocalManagerRegistry.getManager(ArtManagerLocal.class);
+ if (aml == null) {
+ Log.w(LOG_TAG, "Couldn't get ArtManagerLocal");
+ return;
+ }
+ aml.setBatchDexoptStartCallback(ForkJoinPool.commonPool(),
+ (snapshot, reason, defaultPackages, builder, passedSignal) -> {
+ traceOnDex2oatStart();
+ });
+ }
+
+ private void traceOnDex2oatStart() {
+ if (mIProfcollect == null) {
+ return;
+ }
+ // Sample for a fraction of dex2oat runs.
+ final int traceFrequency =
+ DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
+ "dex2oat_trace_freq", 10);
+ int randomNum = ThreadLocalRandom.current().nextInt(100);
+ if (randomNum < traceFrequency) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Tracing on dex2oat event");
+ }
+ BackgroundThread.get().getThreadHandler().post(() -> {
+ try {
+ // Dex2oat could take a while before it starts. Add a short delay before start
+ // tracing.
+ Thread.sleep(1000);
+ mIProfcollect.trace_once("dex2oat");
+ } catch (RemoteException | InterruptedException e) {
+ Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
+ }
+ });
+ }
+ }
+
private void registerOTAObserver() {
UpdateEngine updateEngine = new UpdateEngine();
updateEngine.bind(new UpdateEngineCallback() {
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
index 3c8f5c9..30afa72 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
@@ -15,9 +15,16 @@
*/
package com.android.server.inputmethod;
+import static com.android.server.inputmethod.ClientController.ClientControllerCallback;
+import static com.android.server.inputmethod.ClientController.ClientState;
+
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.pm.PackageManagerInternal;
@@ -38,6 +45,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
// This test is designed to run on both device and host (Ravenwood) side.
public final class ClientControllerTest {
@@ -58,9 +67,6 @@
@Mock
private IRemoteInputConnection mConnection;
- @Mock
- private IBinder.DeathRecipient mDeathRecipient;
-
private Handler mHandler;
private ClientController mController;
@@ -68,9 +74,10 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ when(mClient.asBinder()).thenReturn((IBinder) mClient);
+
mHandler = new Handler(Looper.getMainLooper());
mController = new ClientController(mMockPackageManagerInternal);
- when(mClient.asBinder()).thenReturn((IBinder) mClient);
}
@Test
@@ -80,18 +87,77 @@
var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
synchronized (ImfLock.class) {
- mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, mDeathRecipient,
- ANY_CALLER_UID, ANY_CALLER_PID);
+ mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
+ ANY_CALLER_PID);
SecurityException thrown = assertThrows(SecurityException.class,
() -> {
synchronized (ImfLock.class) {
mController.addClient(invoker, mConnection, ANY_DISPLAY_ID,
- mDeathRecipient, ANY_CALLER_UID, ANY_CALLER_PID);
+ ANY_CALLER_UID, ANY_CALLER_PID);
}
});
assertThat(thrown.getMessage()).isEqualTo(
"uid=1/pid=1/displayId=0 is already registered");
}
}
+
+ @Test
+ // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed.
+ @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
+ public void testAddClient() throws Exception {
+ synchronized (ImfLock.class) {
+ var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
+ var added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
+ ANY_CALLER_PID);
+
+ verify(invoker.asBinder()).linkToDeath(any(IBinder.DeathRecipient.class), eq(0));
+ assertThat(mController.mClients).containsEntry(invoker.asBinder(), added);
+ }
+ }
+
+ @Test
+ // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed.
+ @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
+ public void testRemoveClient() {
+ var callback = new TestClientControllerCallback();
+ ClientState added;
+ synchronized (ImfLock.class) {
+ mController.addClientControllerCallback(callback);
+
+ var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
+ added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
+ ANY_CALLER_PID);
+ assertThat(mController.mClients).containsEntry(invoker.asBinder(), added);
+ assertThat(mController.removeClient(mClient)).isTrue();
+ }
+
+ // Test callback
+ var removed = callback.waitForRemovedClient(5, TimeUnit.SECONDS);
+ assertThat(removed).isSameInstanceAs(added);
+ }
+
+ private static class TestClientControllerCallback implements ClientControllerCallback {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+
+ private ClientState mRemoved;
+
+ @Override
+ public void onClientRemoved(ClientState removed) {
+ mRemoved = removed;
+ mLatch.countDown();
+ }
+
+ ClientState waitForRemovedClient(long timeout, TimeUnit unit) {
+ try {
+ assertWithMessage("ClientController callback wasn't called on user removed").that(
+ mLatch.await(timeout, unit)).isTrue();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Unexpected thread interruption", e);
+ }
+ return mRemoved;
+ }
+ }
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index c67e7c5..b29fc88 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -824,6 +824,16 @@
mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
AUTO_BRIGHTNESS_MODE_DOZE,
Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_BRIGHT), SMALL_DELTA);
+
+ // Should fall back to the normal preset
+ assertArrayEquals(new float[]{0.0f, 95},
+ mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+ AUTO_BRIGHTNESS_MODE_DOZE,
+ Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), ZERO_DELTA);
+ assertArrayEquals(new float[]{0.35f, 0.45f},
+ mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+ AUTO_BRIGHTNESS_MODE_DOZE,
+ Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), SMALL_DELTA);
}
@Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt
new file mode 100644
index 0000000..638924e
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 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.display.mode
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.hardware.display.BrightnessInfo
+import android.view.Display
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.feature.DisplayManagerFlags
+import com.android.server.testutils.TestHandler
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class BrightnessObserverTest {
+
+ @get:Rule
+ val mockitoRule = MockitoJUnit.rule()
+
+ private lateinit var spyContext: Context
+ private val mockInjector = mock<DisplayModeDirector.Injector>()
+ private val mockFlags = mock<DisplayManagerFlags>()
+ private val mockDeviceConfig = mock<DisplayDeviceConfig>()
+
+ private val testHandler = TestHandler(null)
+
+ @Before
+ fun setUp() {
+ spyContext = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+ }
+
+ @Test
+ fun testLowLightBlockingZoneVotes(@TestParameter testCase: LowLightTestCase) {
+ setUpLowBrightnessZone()
+ whenever(mockFlags.isVsyncLowLightVoteEnabled).thenReturn(testCase.vsyncLowLightVoteEnabled)
+ val displayModeDirector = DisplayModeDirector(
+ spyContext, testHandler, mockInjector, mockFlags)
+ val brightnessObserver = displayModeDirector.BrightnessObserver(
+ spyContext, testHandler, mockInjector, testCase.vrrSupported, mockFlags)
+
+ brightnessObserver.onRefreshRateSettingChangedLocked(0.0f, 120.0f)
+ brightnessObserver.updateBlockingZoneThresholds(mockDeviceConfig, false)
+ brightnessObserver.onDeviceConfigRefreshRateInLowZoneChanged(60)
+
+ brightnessObserver.onDisplayChanged(Display.DEFAULT_DISPLAY)
+
+ assertThat(displayModeDirector.getVote(VotesStorage.GLOBAL_ID,
+ Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH)).isEqualTo(testCase.expectedVote)
+ }
+
+ private fun setUpLowBrightnessZone() {
+ whenever(mockInjector.getBrightnessInfo(Display.DEFAULT_DISPLAY)).thenReturn(
+ BrightnessInfo(/* brightness = */ 0.05f, /* adjustedBrightness = */ 0.05f,
+ /* brightnessMinimum = */ 0.0f, /* brightnessMaximum = */ 1.0f,
+ BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+ /* highBrightnessTransitionPoint = */ 1.0f,
+ BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE))
+ whenever(mockDeviceConfig.highDisplayBrightnessThresholds).thenReturn(floatArrayOf())
+ whenever(mockDeviceConfig.highAmbientBrightnessThresholds).thenReturn(floatArrayOf())
+ whenever(mockDeviceConfig.lowDisplayBrightnessThresholds).thenReturn(floatArrayOf(0.1f))
+ whenever(mockDeviceConfig.lowAmbientBrightnessThresholds).thenReturn(floatArrayOf(10f))
+ }
+
+ enum class LowLightTestCase(
+ val vrrSupported: Boolean,
+ val vsyncLowLightVoteEnabled: Boolean,
+ internal val expectedVote: Vote
+ ) {
+ ALL_ENABLED(true, true, CombinedVote(
+ listOf(DisableRefreshRateSwitchingVote(true),
+ SupportedModesVote(
+ listOf(SupportedModesVote.SupportedMode(60f, 60f),
+ SupportedModesVote.SupportedMode(120f, 120f)))))),
+ VRR_NOT_SUPPORTED(false, true, DisableRefreshRateSwitchingVote(true)),
+ VSYNC_VOTE_DISABLED(true, false, DisableRefreshRateSwitchingVote(true))
+ }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java
index ff91d34..92016df 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java
@@ -20,11 +20,10 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.Mode.INVALID_MODE_ID;
-
import static com.android.server.display.mode.DisplayModeDirector.SYNCHRONIZED_REFRESH_RATE_TOLERANCE;
import static com.android.server.display.mode.Vote.PRIORITY_LIMIT_MODE;
-import static com.android.server.display.mode.Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE;
import static com.android.server.display.mode.Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE;
+import static com.android.server.display.mode.Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE;
import static com.android.server.display.mode.VotesStorage.GLOBAL_ID;
import static com.google.common.truth.Truth.assertThat;
@@ -43,6 +42,7 @@
import android.os.Handler;
import android.os.Looper;
import android.provider.DeviceConfigInterface;
+import android.test.mock.MockContentResolver;
import android.view.Display;
import android.view.DisplayInfo;
@@ -51,21 +51,26 @@
import androidx.test.filters.SmallTest;
import com.android.internal.R;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.internal.util.test.FakeSettingsProviderRule;
import com.android.server.display.feature.DisplayManagerFlags;
import com.android.server.sensors.SensorManagerInternal;
+import junitparams.JUnitParamsRunner;
+
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import junitparams.JUnitParamsRunner;
-
-
@SmallTest
@RunWith(JUnitParamsRunner.class)
public class DisplayObserverTest {
+ @Rule
+ public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
+
private static final int EXTERNAL_DISPLAY = 1;
private static final int MAX_WIDTH = 1920;
private static final int MAX_HEIGHT = 1080;
@@ -120,6 +125,8 @@
mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
mResources = mock(Resources.class);
when(mContext.getResources()).thenReturn(mResources);
+ MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContext);
+ when(mContext.getContentResolver()).thenReturn(resolver);
when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate))
.thenReturn(0);
when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth))
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java
index e2c338a..7e1dc08 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java
@@ -202,7 +202,7 @@
final ServiceInfo regularService = new ServiceInfo();
regularService.processName = "com.foo";
String processName = ActiveServices.getProcessNameForService(regularService, null, null,
- null, false, false);
+ null, false, false, false);
assertEquals("com.foo", processName);
// Isolated service
@@ -211,29 +211,90 @@
isolatedService.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
final ComponentName component = new ComponentName("com.foo", "barService");
processName = ActiveServices.getProcessNameForService(isolatedService, component,
- null, null, false, false);
+ null, null, false, false, false);
assertEquals("com.foo:barService", processName);
+ // Isolated Service in package private process.
+ final ServiceInfo isolatedService1 = new ServiceInfo();
+ isolatedService1.processName = "com.foo:trusted_isolated";
+ isolatedService1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
+ final ComponentName componentName = new ComponentName("com.foo", "barService");
+ processName = ActiveServices.getProcessNameForService(isolatedService1, componentName,
+ null, null, false, false, false);
+ assertEquals("com.foo:trusted_isolated:barService", processName);
+
+ // Isolated service in package-private shared process (main process)
+ final ServiceInfo isolatedPackageSharedService = new ServiceInfo();
+ final ComponentName componentName1 = new ComponentName("com.foo", "barService");
+ isolatedPackageSharedService.processName = "com.foo";
+ isolatedPackageSharedService.applicationInfo = new ApplicationInfo();
+ isolatedPackageSharedService.applicationInfo.processName = "com.foo";
+ isolatedPackageSharedService.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
+ String packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+ isolatedPackageSharedService, componentName1, null, null, false, false, true);
+ assertEquals("com.foo:barService", packageSharedIsolatedProcessName);
+
+ // Isolated service in package-private shared process
+ final ServiceInfo isolatedPackageSharedService1 = new ServiceInfo(
+ isolatedPackageSharedService);
+ isolatedPackageSharedService1.processName = "com.foo:trusted_isolated";
+ isolatedPackageSharedService1.applicationInfo = new ApplicationInfo();
+ isolatedPackageSharedService1.applicationInfo.processName = "com.foo";
+ isolatedPackageSharedService1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
+ packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+ isolatedPackageSharedService1, componentName1, null, null, false, false, true);
+ assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName);
+
+
+ // Bind another one in the same isolated process
+ final ServiceInfo isolatedPackageSharedService2 = new ServiceInfo(
+ isolatedPackageSharedService1);
+ packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+ isolatedPackageSharedService2, componentName1, null, null, false, false, true);
+ assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName);
+
+ // Simulate another app trying to do the bind.
+ final ServiceInfo isolatedPackageSharedService3 = new ServiceInfo(
+ isolatedPackageSharedService1);
+ final String auxCallingPackage = "com.bar";
+ packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+ isolatedPackageSharedService3, componentName1, auxCallingPackage, null,
+ false, false, true);
+ assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName);
+
+ // Simulate another app owning the service
+ final ServiceInfo isolatedOtherPackageSharedService = new ServiceInfo(
+ isolatedPackageSharedService1);
+ final ComponentName componentName2 = new ComponentName("com.bar", "barService");
+ isolatedOtherPackageSharedService.processName = "com.bar:isolated";
+ isolatedPackageSharedService.applicationInfo.processName = "com.bar";
+ final String mainCallingPackage = "com.foo";
+ packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+ isolatedOtherPackageSharedService, componentName2, mainCallingPackage,
+ null, false, false, true);
+ assertEquals("com.bar:isolated", packageSharedIsolatedProcessName);
+
// Isolated service in shared isolated process
final ServiceInfo isolatedServiceShared1 = new ServiceInfo();
isolatedServiceShared1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
final String instanceName = "pool";
final String callingPackage = "com.foo";
final String sharedIsolatedProcessName1 = ActiveServices.getProcessNameForService(
- isolatedServiceShared1, null, callingPackage, instanceName, false, true);
+ isolatedServiceShared1, null, callingPackage, instanceName, false, true, false);
assertEquals("com.foo:ishared:pool", sharedIsolatedProcessName1);
// Bind another one in the same isolated process
final ServiceInfo isolatedServiceShared2 = new ServiceInfo(isolatedServiceShared1);
final String sharedIsolatedProcessName2 = ActiveServices.getProcessNameForService(
- isolatedServiceShared2, null, callingPackage, instanceName, false, true);
+ isolatedServiceShared2, null, callingPackage, instanceName, false, true, false);
assertEquals(sharedIsolatedProcessName1, sharedIsolatedProcessName2);
// Simulate another app trying to do the bind
final ServiceInfo isolatedServiceShared3 = new ServiceInfo(isolatedServiceShared1);
final String otherCallingPackage = "com.bar";
final String sharedIsolatedProcessName3 = ActiveServices.getProcessNameForService(
- isolatedServiceShared3, null, otherCallingPackage, instanceName, false, true);
+ isolatedServiceShared3, null, otherCallingPackage, instanceName, false, true,
+ false);
Assert.assertNotEquals(sharedIsolatedProcessName2, sharedIsolatedProcessName3);
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 650c473..116d5db 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -26,15 +26,18 @@
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
+import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
+import static com.android.server.job.controllers.FlexibilityController.FLEXIBLE_CONSTRAINTS;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_APPLIED_CONSTRAINTS;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_DEADLINE_PROXIMITY_LIMIT;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINE;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS;
-import static com.android.server.job.controllers.FlexibilityController.FLEXIBLE_CONSTRAINTS;
import static com.android.server.job.controllers.FlexibilityController.SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BATTERY_NOT_LOW;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING;
@@ -50,24 +53,32 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.AlarmManager;
import android.app.AppGlobals;
import android.app.job.JobInfo;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.net.NetworkRequest;
import android.os.Looper;
+import android.os.PowerManager;
import android.provider.DeviceConfig;
import android.util.ArraySet;
+import android.util.EmptyArray;
import com.android.server.AppSchedulingModuleThread;
+import com.android.server.DeviceIdleInternal;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobStore;
@@ -77,6 +88,7 @@
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoSession;
@@ -95,6 +107,7 @@
private static final long FROZEN_TIME = 100L;
private MockitoSession mMockingSession;
+ private BroadcastReceiver mBroadcastReceiver;
private FlexibilityController mFlexibilityController;
private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder;
private JobStore mJobStore;
@@ -106,6 +119,8 @@
@Mock
private Context mContext;
@Mock
+ private DeviceIdleInternal mDeviceIdleInternal;
+ @Mock
private JobSchedulerService mJobSchedulerService;
@Mock
private PrefetchController mPrefetchController;
@@ -128,10 +143,13 @@
// Called in FlexibilityController constructor.
when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager);
+ doNothing().when(mAlarmManager).setExact(anyInt(), anyLong(), anyString(), any(), any());
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mPackageManager.hasSystemFeature(
PackageManager.FEATURE_AUTOMOTIVE)).thenReturn(false);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_EMBEDDED)).thenReturn(false);
+ doReturn(mDeviceIdleInternal)
+ .when(() -> LocalServices.getService(DeviceIdleInternal.class));
// Used in FlexibilityController.FcConstants.
doAnswer((Answer<Void>) invocationOnMock -> null)
.when(() -> DeviceConfig.addOnPropertiesChangedListener(
@@ -146,7 +164,7 @@
eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.<String>any()));
//used to get jobs by UID
mJobStore = JobStore.initAndGetForTesting(mContext, mContext.getFilesDir());
- when(mJobSchedulerService.getJobStore()).thenReturn(mJobStore);
+ doReturn(mJobStore).when(mJobSchedulerService).getJobStore();
// Used in JobStatus.
doReturn(mock(PackageManagerInternal.class))
.when(() -> LocalServices.getService(PackageManagerInternal.class));
@@ -156,6 +174,8 @@
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC);
// Initialize real objects.
+ ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
mFlexibilityController = new FlexibilityController(mJobSchedulerService,
mPrefetchController);
mFcConfig = mFlexibilityController.getFcConfig();
@@ -166,6 +186,11 @@
setDeviceConfigLong(KEY_DEADLINE_PROXIMITY_LIMIT, 0L);
setDeviceConfigInt(KEY_APPLIED_CONSTRAINTS, FLEXIBLE_CONSTRAINTS);
waitForQuietModuleThread();
+
+ verify(mContext).registerReceiver(receiverCaptor.capture(),
+ ArgumentMatchers.argThat(filter ->
+ filter.hasAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED)));
+ mBroadcastReceiver = receiverCaptor.getValue();
}
@After
@@ -212,6 +237,7 @@
JobStatus js = JobStatus.createFromJobInfo(
jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "FCTest", testTag);
js.enqueueTime = FROZEN_TIME;
+ js.setStandbyBucket(ACTIVE_INDEX);
if (js.hasFlexibilityConstraint()) {
js.setNumAppliedFlexibleConstraints(Integer.bitCount(
mFlexibilityController.getRelevantAppliedConstraintsLocked(js)));
@@ -598,10 +624,10 @@
@Test
public void testGetLifeCycleBeginningElapsedLocked_Prefetch() {
// prefetch with lifecycle
- when(mPrefetchController.getLaunchTimeThresholdMs()).thenReturn(700L);
+ doReturn(700L).when(mPrefetchController).getLaunchTimeThresholdMs();
JobInfo.Builder jb = createJob(0).setPrefetch(true);
JobStatus js = createJobStatus("time", jb);
- when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(900L);
+ doReturn(900L).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
assertEquals(900L - 700L, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
// prefetch with enqueue
jb = createJob(0).setPrefetch(true);
@@ -616,7 +642,7 @@
// prefetch without estimate
mFlexibilityController.mPrefetchLifeCycleStart
.add(js.getUserId(), js.getSourcePackageName(), 500L);
- when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(Long.MAX_VALUE);
+ doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
jb = createJob(0).setPrefetch(true);
js = createJobStatus("time", jb);
assertEquals(500L, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
@@ -642,12 +668,12 @@
// prefetch no estimate
JobInfo.Builder jb = createJob(0).setPrefetch(true);
JobStatus js = createJobStatus("time", jb);
- when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(Long.MAX_VALUE);
+ doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
assertEquals(Long.MAX_VALUE, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
// prefetch with estimate
jb = createJob(0).setPrefetch(true);
js = createJobStatus("time", jb);
- when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(1000L);
+ doReturn(1000L).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
assertEquals(1000L, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
}
@@ -696,7 +722,7 @@
// Stop satisfied constraints from causing a false positive.
js.setNumAppliedFlexibleConstraints(100);
synchronized (mFlexibilityController.mLock) {
- when(mJobSchedulerService.isCurrentlyRunningLocked(js)).thenReturn(true);
+ doReturn(true).when(mJobSchedulerService).isCurrentlyRunningLocked(js);
assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(js));
}
}
@@ -847,14 +873,85 @@
}
@Test
+ public void testAllowlistedAppBypass() {
+ setPowerWhitelistExceptIdle();
+ mFlexibilityController.onSystemServicesReady();
+
+ JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass",
+ createJob(0).setPriority(JobInfo.PRIORITY_HIGH));
+ JobStatus jsDefault = createJobStatus("testAllowlistedAppBypass",
+ createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT));
+ JobStatus jsLow = createJobStatus("testAllowlistedAppBypass",
+ createJob(0).setPriority(JobInfo.PRIORITY_LOW));
+ JobStatus jsMin = createJobStatus("testAllowlistedAppBypass",
+ createJob(0).setPriority(JobInfo.PRIORITY_MIN));
+ jsHigh.setStandbyBucket(EXEMPTED_INDEX);
+ jsDefault.setStandbyBucket(EXEMPTED_INDEX);
+ jsLow.setStandbyBucket(EXEMPTED_INDEX);
+ jsMin.setStandbyBucket(EXEMPTED_INDEX);
+
+ setPowerWhitelistExceptIdle();
+ synchronized (mFlexibilityController.mLock) {
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+ }
+
+ setPowerWhitelistExceptIdle(SOURCE_PACKAGE);
+ synchronized (mFlexibilityController.mLock) {
+ assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+ assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+ }
+ }
+
+ @Test
+ public void testForegroundAppBypass() {
+ JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass",
+ createJob(0).setPriority(JobInfo.PRIORITY_HIGH));
+ JobStatus jsDefault = createJobStatus("testAllowlistedAppBypass",
+ createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT));
+ JobStatus jsLow = createJobStatus("testAllowlistedAppBypass",
+ createJob(0).setPriority(JobInfo.PRIORITY_LOW));
+ JobStatus jsMin = createJobStatus("testAllowlistedAppBypass",
+ createJob(0).setPriority(JobInfo.PRIORITY_MIN));
+
+ doReturn(JobInfo.BIAS_DEFAULT).when(mJobSchedulerService).getUidBias(mSourceUid);
+ synchronized (mFlexibilityController.mLock) {
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+ }
+
+ setUidBias(mSourceUid, JobInfo.BIAS_BOUND_FOREGROUND_SERVICE);
+ synchronized (mFlexibilityController.mLock) {
+ assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+ assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+ }
+
+ setUidBias(mSourceUid, JobInfo.BIAS_FOREGROUND_SERVICE);
+ synchronized (mFlexibilityController.mLock) {
+ assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+ assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+ assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+ }
+ }
+
+ @Test
public void testTopAppBypass() {
- JobInfo.Builder jb = createJob(0);
+ JobInfo.Builder jb = createJob(0).setPriority(JobInfo.PRIORITY_MIN);
JobStatus js = createJobStatus("testTopAppBypass", jb);
mJobStore.add(js);
// Needed because if before and after Uid bias is the same, nothing happens.
when(mJobSchedulerService.getUidBias(mSourceUid))
- .thenReturn(JobInfo.BIAS_FOREGROUND_SERVICE);
+ .thenReturn(JobInfo.BIAS_DEFAULT);
synchronized (mFlexibilityController.mLock) {
mFlexibilityController.maybeStartTrackingJobLocked(js, null);
@@ -865,7 +962,7 @@
assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(js));
assertTrue(js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE));
- setUidBias(mSourceUid, JobInfo.BIAS_FOREGROUND_SERVICE);
+ setUidBias(mSourceUid, JobInfo.BIAS_SYNC_INITIALIZATION);
assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(js));
assertFalse(js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE));
@@ -1187,9 +1284,9 @@
JobInfo.Builder jb = createJob(22).setPrefetch(true);
JobStatus js = createJobStatus("onPrefetchCacheUpdated", jb);
jobs.add(js);
- when(mPrefetchController.getLaunchTimeThresholdMs()).thenReturn(7 * HOUR_IN_MILLIS);
- when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(
- 1150L + mFlexibilityController.mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS);
+ doReturn(7 * HOUR_IN_MILLIS).when(mPrefetchController).getLaunchTimeThresholdMs();
+ doReturn(1150L + mFlexibilityController.mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS)
+ .when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js);
mFlexibilityController.maybeStartTrackingJobLocked(js, null);
@@ -1245,7 +1342,6 @@
setUidBias(mSourceUid, BIAS_FOREGROUND_SERVICE);
assertEquals(100L, (long) mFlexibilityController
.mPrefetchLifeCycleStart.get(js.getSourceUserId(), js.getSourcePackageName()));
-
}
@Test
@@ -1259,7 +1355,7 @@
}
private void runTestUnsupportedDevice(String feature) {
- when(mPackageManager.hasSystemFeature(feature)).thenReturn(true);
+ doReturn(true).when(mPackageManager).hasSystemFeature(feature);
mFlexibilityController =
new FlexibilityController(mJobSchedulerService, mPrefetchController);
assertFalse(mFlexibilityController.isEnabled());
@@ -1279,6 +1375,16 @@
}
}
+ private void setPowerWhitelistExceptIdle(String... packages) {
+ doReturn(packages == null ? EmptyArray.STRING : packages)
+ .when(mDeviceIdleInternal).getFullPowerWhitelistExceptIdle();
+ if (mBroadcastReceiver != null) {
+ mBroadcastReceiver.onReceive(mContext,
+ new Intent(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED));
+ waitForQuietModuleThread();
+ }
+ }
+
private void setUidBias(int uid, int bias) {
int prevBias = mJobSchedulerService.getUidBias(uid);
doReturn(bias).when(mJobSchedulerService).getUidBias(uid);
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index e1f490a..d71844b 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -608,6 +608,20 @@
}
@Test
+ public void testIsInputDeviceOwnedByVirtualDevice() {
+ assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isFalse();
+
+ final int fd = 1;
+ mInputController.addDeviceForTesting(BINDER, fd,
+ InputController.InputDeviceDescriptor.TYPE_KEYBOARD, DISPLAY_ID_1, PHYS,
+ DEVICE_NAME_1, INPUT_DEVICE_ID);
+ assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isTrue();
+
+ mInputController.unregisterInputDevice(BINDER);
+ assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isFalse();
+ }
+
+ @Test
public void getDeviceIdsForUid_noRunningApps_returnsNull() {
assertThat(mLocalService.getDeviceIdsForUid(UID_1)).isEmpty();
assertThat(mVdmNative.getDeviceIdsForUid(UID_1)).isEmpty();
@@ -1957,7 +1971,7 @@
mRunningAppsChangedCallback,
params,
new DisplayManagerGlobal(mIDisplayManager),
- new VirtualCameraController());
+ new VirtualCameraController(DEVICE_POLICY_DEFAULT));
mVdms.addVirtualDevice(virtualDeviceImpl);
assertThat(virtualDeviceImpl.getAssociationId()).isEqualTo(mAssociationInfo.getId());
assertThat(virtualDeviceImpl.getPersistentDeviceId())
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
index 9b28b81..3e4f1df 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
@@ -16,27 +16,35 @@
package com.android.server.companion.virtual.camera;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.camera.VirtualCameraConfig.SENSOR_ORIENTATION_0;
+import static android.companion.virtual.camera.VirtualCameraConfig.SENSOR_ORIENTATION_90;
+import static android.graphics.ImageFormat.YUV_420_888;
+import static android.graphics.PixelFormat.RGBA_8888;
+import static android.hardware.camera2.CameraMetadata.LENS_FACING_BACK;
+import static android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT;
+
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.annotation.NonNull;
import android.companion.virtual.camera.VirtualCameraCallback;
import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.camera.VirtualCameraStreamConfig;
import android.companion.virtualcamera.IVirtualCameraService;
import android.companion.virtualcamera.VirtualCameraConfiguration;
-import android.graphics.ImageFormat;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.platform.test.annotations.Presubmit;
-import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
-import android.view.Surface;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
import org.junit.After;
import org.junit.Before;
@@ -49,21 +57,30 @@
import java.util.List;
@Presubmit
-@RunWith(AndroidTestingRunner.class)
+@RunWith(JUnitParamsRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class VirtualCameraControllerTest {
private static final String CAMERA_NAME_1 = "Virtual camera 1";
private static final int CAMERA_WIDTH_1 = 100;
private static final int CAMERA_HEIGHT_1 = 200;
+ private static final int CAMERA_FORMAT_1 = YUV_420_888;
+ private static final int CAMERA_MAX_FPS_1 = 30;
+ private static final int CAMERA_SENSOR_ORIENTATION_1 = SENSOR_ORIENTATION_0;
+ private static final int CAMERA_LENS_FACING_1 = LENS_FACING_BACK;
private static final String CAMERA_NAME_2 = "Virtual camera 2";
private static final int CAMERA_WIDTH_2 = 400;
private static final int CAMERA_HEIGHT_2 = 600;
- private static final int CAMERA_FORMAT = ImageFormat.YUV_420_888;
+ private static final int CAMERA_FORMAT_2 = RGBA_8888;
+ private static final int CAMERA_MAX_FPS_2 = 60;
+ private static final int CAMERA_SENSOR_ORIENTATION_2 = SENSOR_ORIENTATION_90;
+ private static final int CAMERA_LENS_FACING_2 = LENS_FACING_FRONT;
@Mock
private IVirtualCameraService mVirtualCameraServiceMock;
+ @Mock
+ private VirtualCameraCallback mVirtualCameraCallbackMock;
private VirtualCameraController mVirtualCameraController;
private final HandlerExecutor mCallbackHandler =
@@ -72,7 +89,8 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock);
+ mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock,
+ DEVICE_POLICY_CUSTOM);
when(mVirtualCameraServiceMock.registerCamera(any(), any())).thenReturn(true);
}
@@ -81,10 +99,12 @@
mVirtualCameraController.close();
}
+ @Parameters(method = "getAllLensFacingDirections")
@Test
- public void registerCamera_registersCamera() throws Exception {
+ public void registerCamera_registersCamera(int lensFacing) throws Exception {
mVirtualCameraController.registerCamera(createVirtualCameraConfig(
- CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1));
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+ CAMERA_SENSOR_ORIENTATION_1, lensFacing));
ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
@@ -92,13 +112,15 @@
VirtualCameraConfiguration virtualCameraConfiguration = configurationCaptor.getValue();
assertThat(virtualCameraConfiguration.supportedStreamConfigs.length).isEqualTo(1);
assertVirtualCameraConfiguration(virtualCameraConfiguration, CAMERA_WIDTH_1,
- CAMERA_HEIGHT_1, CAMERA_FORMAT);
+ CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_SENSOR_ORIENTATION_1,
+ lensFacing);
}
@Test
public void unregisterCamera_unregistersCamera() throws Exception {
VirtualCameraConfig config = createVirtualCameraConfig(
- CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1);
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+ CAMERA_SENSOR_ORIENTATION_1, CAMERA_LENS_FACING_1);
mVirtualCameraController.registerCamera(config);
mVirtualCameraController.unregisterCamera(config);
@@ -109,9 +131,11 @@
@Test
public void close_unregistersAllCameras() throws Exception {
mVirtualCameraController.registerCamera(createVirtualCameraConfig(
- CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1));
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+ CAMERA_SENSOR_ORIENTATION_1, CAMERA_LENS_FACING_1));
mVirtualCameraController.registerCamera(createVirtualCameraConfig(
- CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT, CAMERA_NAME_2));
+ CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2, CAMERA_NAME_2,
+ CAMERA_SENSOR_ORIENTATION_2, CAMERA_LENS_FACING_2));
mVirtualCameraController.close();
@@ -123,38 +147,66 @@
configurationCaptor.getAllValues();
assertThat(virtualCameraConfigurations).hasSize(2);
assertVirtualCameraConfiguration(virtualCameraConfigurations.get(0), CAMERA_WIDTH_1,
- CAMERA_HEIGHT_1, CAMERA_FORMAT);
+ CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_SENSOR_ORIENTATION_1,
+ CAMERA_LENS_FACING_1);
assertVirtualCameraConfiguration(virtualCameraConfigurations.get(1), CAMERA_WIDTH_2,
- CAMERA_HEIGHT_2, CAMERA_FORMAT);
+ CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2, CAMERA_SENSOR_ORIENTATION_2,
+ CAMERA_LENS_FACING_2);
+ }
+
+ @Parameters(method = "getAllLensFacingDirections")
+ @Test
+ public void registerMultipleSameLensFacingCameras_withCustomCameraPolicy_throwsException(
+ int lensFacing) {
+ mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+ CAMERA_SENSOR_ORIENTATION_1, lensFacing));
+ assertThrows(IllegalArgumentException.class,
+ () -> mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+ CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2,
+ CAMERA_NAME_2, CAMERA_SENSOR_ORIENTATION_2, lensFacing)));
+ }
+
+ @Parameters(method = "getAllLensFacingDirections")
+ @Test
+ public void registerCamera_withDefaultCameraPolicy_throwsException(int lensFacing) {
+ mVirtualCameraController.close();
+ mVirtualCameraController = new VirtualCameraController(
+ mVirtualCameraServiceMock, DEVICE_POLICY_DEFAULT);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1,
+ CAMERA_NAME_1, CAMERA_SENSOR_ORIENTATION_1, lensFacing)));
}
private VirtualCameraConfig createVirtualCameraConfig(
- int width, int height, int format, String displayName) {
+ int width, int height, int format, int maximumFramesPerSecond,
+ String name, int sensorOrientation, int lensFacing) {
return new VirtualCameraConfig.Builder()
- .addStreamConfig(width, height, format)
- .setName(displayName)
- .setVirtualCameraCallback(mCallbackHandler, createNoOpCallback())
+ .addStreamConfig(width, height, format, maximumFramesPerSecond)
+ .setName(name)
+ .setVirtualCameraCallback(mCallbackHandler, mVirtualCameraCallbackMock)
+ .setSensorOrientation(sensorOrientation)
+ .setLensFacing(lensFacing)
.build();
}
private static void assertVirtualCameraConfiguration(
- VirtualCameraConfiguration configuration, int width, int height, int format) {
+ VirtualCameraConfiguration configuration, int width, int height, int format,
+ int maxFps, int sensorOrientation, int lensFacing) {
assertThat(configuration.supportedStreamConfigs[0].width).isEqualTo(width);
assertThat(configuration.supportedStreamConfigs[0].height).isEqualTo(height);
assertThat(configuration.supportedStreamConfigs[0].pixelFormat).isEqualTo(format);
+ assertThat(configuration.supportedStreamConfigs[0].maxFps).isEqualTo(maxFps);
+ assertThat(configuration.sensorOrientation).isEqualTo(sensorOrientation);
+ assertThat(configuration.lensFacing).isEqualTo(lensFacing);
}
- private static VirtualCameraCallback createNoOpCallback() {
- return new VirtualCameraCallback() {
-
- @Override
- public void onStreamConfigured(
- int streamId,
- @NonNull Surface surface,
- @NonNull VirtualCameraStreamConfig streamConfig) {}
-
- @Override
- public void onStreamClosed(int streamId) {}
+ private static Integer[] getAllLensFacingDirections() {
+ return new Integer[] {
+ LENS_FACING_BACK,
+ LENS_FACING_FRONT
};
}
}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
index d9a38eb..206c111 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
@@ -35,19 +35,20 @@
private static final int VGA_WIDTH = 640;
private static final int VGA_HEIGHT = 480;
+ private static final int MAX_FPS_1 = 30;
private static final int QVGA_WIDTH = 320;
private static final int QVGA_HEIGHT = 240;
+ private static final int MAX_FPS_2 = 60;
@Test
public void testEquals() {
VirtualCameraStreamConfig vgaYuvStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH,
- VGA_HEIGHT,
- ImageFormat.YUV_420_888);
+ VGA_HEIGHT, ImageFormat.YUV_420_888, MAX_FPS_1);
VirtualCameraStreamConfig qvgaYuvStreamConfig = new VirtualCameraStreamConfig(QVGA_WIDTH,
- QVGA_HEIGHT, ImageFormat.YUV_420_888);
+ QVGA_HEIGHT, ImageFormat.YUV_420_888, MAX_FPS_2);
VirtualCameraStreamConfig vgaRgbaStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH,
- VGA_HEIGHT, PixelFormat.RGBA_8888);
+ VGA_HEIGHT, PixelFormat.RGBA_8888, MAX_FPS_1);
new EqualsTester()
.addEqualityGroup(vgaYuvStreamConfig, reparcel(vgaYuvStreamConfig))
@@ -66,6 +67,4 @@
parcel.recycle();
}
}
-
-
}
diff --git a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
index 769ec5f..3218586 100644
--- a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
@@ -345,15 +345,18 @@
intent, UID_PRIMARY_CAMERA, PKG_SOCIAL, USER_PRIMARY), service);
// Verify that everything is good with the world
- assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ));
+ assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ,
+ /* isFullAccessForContentUri */ false));
// Finish activity; service should hold permission
activity.removeUriPermissions();
- assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ));
+ assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ,
+ /* isFullAccessForContentUri */ false));
// And finishing service should wrap things up
service.removeUriPermissions();
- assertFalse(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ));
+ assertFalse(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ,
+ /* isFullAccessForContentUri */ false));
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
index 3530e38..ae0a758 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
@@ -85,7 +85,7 @@
private void enablePackageForUser(String packageName, boolean enable, int userId) {
Map<Integer, PackageInfo> userPackages = mPackages.get(packageName);
if (userPackages == null) {
- throw new IllegalArgumentException("There is no package called " + packageName);
+ return;
}
PackageInfo packageInfo = userPackages.get(userId);
packageInfo.applicationInfo.enabled = enable;
diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
index 32082e3..5a06327 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
@@ -127,12 +127,21 @@
private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName,
WebViewProviderInfo[] webviewPackages) {
checkCertainPackageUsedAfterWebViewBootPreparation(
- expectedProviderName, webviewPackages, 1);
+ expectedProviderName, webviewPackages, 1, null);
}
private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName,
- WebViewProviderInfo[] webviewPackages, int numRelros) {
+ WebViewProviderInfo[] webviewPackages, String userSetting) {
+ checkCertainPackageUsedAfterWebViewBootPreparation(
+ expectedProviderName, webviewPackages, 1, userSetting);
+ }
+
+ private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName,
+ WebViewProviderInfo[] webviewPackages, int numRelros, String userSetting) {
setupWithPackagesAndRelroCount(webviewPackages, numRelros);
+ if (userSetting != null) {
+ mTestSystemImpl.updateUserSetting(null, userSetting);
+ }
// Add (enabled and valid) package infos for each provider
setEnabledAndValidPackageInfos(webviewPackages);
@@ -280,7 +289,7 @@
singlePackage,
new WebViewProviderInfo[] {
new WebViewProviderInfo(singlePackage, "", true /*def av*/, false, null)},
- 2);
+ 2, null);
}
// Ensure that package with valid signatures is chosen rather than package with invalid
@@ -295,14 +304,16 @@
Signature invalidPackageSignature = new Signature("33");
WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
- new WebViewProviderInfo(invalidPackage, "", true, false, new String[]{
- Base64.encodeToString(
- invalidExpectedSignature.toByteArray(), Base64.DEFAULT)}),
new WebViewProviderInfo(validPackage, "", true, false, new String[]{
Base64.encodeToString(
- validSignature.toByteArray(), Base64.DEFAULT)})
+ validSignature.toByteArray(), Base64.DEFAULT)}),
+ new WebViewProviderInfo(invalidPackage, "", true, false, new String[]{
+ Base64.encodeToString(
+ invalidExpectedSignature.toByteArray(), Base64.DEFAULT)})
};
setupWithPackagesNonDebuggable(packages);
+ // Start with the setting pointing to the invalid package
+ mTestSystemImpl.updateUserSetting(null, invalidPackage);
mTestSystemImpl.setPackageInfo(createPackageInfo(invalidPackage, true /* enabled */,
true /* valid */, true /* installed */, new Signature[]{invalidPackageSignature}
, 0 /* updateTime */));
@@ -339,7 +350,9 @@
}
@Test
- public void testFailListingEmptyWebviewPackages() {
+ @RequiresFlagsDisabled("android.webkit.update_service_v2")
+ // If the flag is set, will throw an exception because of no available by default provider.
+ public void testEmptyConfig() {
WebViewProviderInfo[] packages = new WebViewProviderInfo[0];
setupWithPackages(packages);
setEnabledAndValidPackageInfos(packages);
@@ -352,14 +365,26 @@
WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status);
assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage());
+ }
- // Now install a package
+ @Test
+ public void testFailListingEmptyWebviewPackages() {
String singlePackage = "singlePackage";
- packages = new WebViewProviderInfo[]{
+ WebViewProviderInfo[] packages = new WebViewProviderInfo[]{
new WebViewProviderInfo(singlePackage, "", true, false, null)};
setupWithPackages(packages);
- setEnabledAndValidPackageInfos(packages);
+ runWebViewBootPreparationOnMainSync();
+
+ Mockito.verify(mTestSystemImpl, Mockito.never()).onWebViewProviderChanged(
+ Matchers.anyObject());
+
+ WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
+ assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status);
+ assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage());
+
+ // Now install the package
+ setEnabledAndValidPackageInfos(packages);
mWebViewUpdateServiceImpl.packageStateChanged(singlePackage,
WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID);
@@ -370,7 +395,7 @@
// Remove the package again
mTestSystemImpl.removePackageInfo(singlePackage);
mWebViewUpdateServiceImpl.packageStateChanged(singlePackage,
- WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID);
+ WebViewUpdateService.PACKAGE_REMOVED, TestSystemImpl.PRIMARY_USER_ID);
// Package removed - ensure our interface states that there is no package
response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
@@ -455,6 +480,8 @@
new WebViewProviderInfo(firstPackage, "", true, false, null),
new WebViewProviderInfo(secondPackage, "", true, false, null)};
setupWithPackages(packages);
+ // Start with the setting pointing to the second package
+ mTestSystemImpl.updateUserSetting(null, secondPackage);
// Have all packages be enabled, so that we can change provider however we want to
setEnabledAndValidPackageInfos(packages);
@@ -463,9 +490,9 @@
runWebViewBootPreparationOnMainSync();
Mockito.verify(mTestSystemImpl).onWebViewProviderChanged(
- Mockito.argThat(new IsPackageInfoWithName(firstPackage)));
+ Mockito.argThat(new IsPackageInfoWithName(secondPackage)));
- assertEquals(firstPackage,
+ assertEquals(secondPackage,
mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName);
new Thread(new Runnable() {
@@ -474,12 +501,13 @@
WebViewProviderResponse threadResponse =
mWebViewUpdateServiceImpl.waitForAndGetProvider();
assertEquals(WebViewFactory.LIBLOAD_SUCCESS, threadResponse.status);
- assertEquals(secondPackage, threadResponse.packageInfo.packageName);
- // Verify that we killed the first package if we performed a settings change -
- // otherwise we had to disable the first package, in which case its dependents
+ assertEquals(firstPackage, threadResponse.packageInfo.packageName);
+ // Verify that we killed the second package if we performed a settings change -
+ // otherwise we had to disable the second package, in which case its dependents
// should have been killed by the framework.
if (settingsChange) {
- Mockito.verify(mTestSystemImpl).killPackageDependents(Mockito.eq(firstPackage));
+ Mockito.verify(mTestSystemImpl)
+ .killPackageDependents(Mockito.eq(secondPackage));
}
countdown.countDown();
}
@@ -490,32 +518,36 @@
}
if (settingsChange) {
- mWebViewUpdateServiceImpl.changeProviderAndSetting(secondPackage);
+ mWebViewUpdateServiceImpl.changeProviderAndSetting(firstPackage);
} else {
- // Enable the second provider
- mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, true /* enabled */,
+ // Enable the first provider
+ mTestSystemImpl.setPackageInfo(createPackageInfo(firstPackage, true /* enabled */,
true /* valid */, true /* installed */));
mWebViewUpdateServiceImpl.packageStateChanged(
- secondPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID);
+ firstPackage,
+ WebViewUpdateService.PACKAGE_CHANGED,
+ TestSystemImpl.PRIMARY_USER_ID);
// Ensure we haven't changed package yet.
- assertEquals(firstPackage,
+ assertEquals(secondPackage,
mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName);
- // Switch provider by disabling the first one
- mTestSystemImpl.setPackageInfo(createPackageInfo(firstPackage, false /* enabled */,
+ // Switch provider by disabling the second one
+ mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, false /* enabled */,
true /* valid */, true /* installed */));
mWebViewUpdateServiceImpl.packageStateChanged(
- firstPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID);
+ secondPackage,
+ WebViewUpdateService.PACKAGE_CHANGED,
+ TestSystemImpl.PRIMARY_USER_ID);
}
mWebViewUpdateServiceImpl.notifyRelroCreationCompleted();
- // first package done, should start on second
+ // second package done, should start on first
Mockito.verify(mTestSystemImpl).onWebViewProviderChanged(
- Mockito.argThat(new IsPackageInfoWithName(secondPackage)));
+ Mockito.argThat(new IsPackageInfoWithName(firstPackage)));
mWebViewUpdateServiceImpl.notifyRelroCreationCompleted();
- // second package done, the other thread should now be unblocked
+ // first package done, the other thread should now be unblocked
try {
countdown.await();
} catch (InterruptedException e) {
@@ -526,6 +558,7 @@
* Scenario for testing re-enabling a fallback package.
*/
@Test
+ @RequiresFlagsDisabled("android.webkit.update_service_v2")
public void testFallbackPackageEnabling() {
String testPackage = "testFallback";
WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
@@ -555,6 +588,9 @@
* 3. Primary should be used
*/
@Test
+ @RequiresFlagsDisabled("android.webkit.update_service_v2")
+ // If the flag is set, we don't automitally switch to secondary package unless it is
+ // chosen directly.
public void testInstallingPrimaryPackage() {
String primaryPackage = "primary";
String secondaryPackage = "secondary";
@@ -586,16 +622,16 @@
}
@Test
- public void testRemovingPrimarySelectsSecondarySingleUser() {
+ public void testRemovingSecondarySelectsPrimarySingleUser() {
for (PackageRemovalType removalType : REMOVAL_TYPES) {
- checkRemovingPrimarySelectsSecondary(false /* multiUser */, removalType);
+ checkRemovingSecondarySelectsPrimary(false /* multiUser */, removalType);
}
}
@Test
- public void testRemovingPrimarySelectsSecondaryMultiUser() {
+ public void testRemovingSecondarySelectsPrimaryMultiUser() {
for (PackageRemovalType removalType : REMOVAL_TYPES) {
- checkRemovingPrimarySelectsSecondary(true /* multiUser */, removalType);
+ checkRemovingSecondarySelectsPrimary(true /* multiUser */, removalType);
}
}
@@ -609,7 +645,7 @@
private PackageRemovalType[] REMOVAL_TYPES = PackageRemovalType.class.getEnumConstants();
- public void checkRemovingPrimarySelectsSecondary(boolean multiUser,
+ private void checkRemovingSecondarySelectsPrimary(boolean multiUser,
PackageRemovalType removalType) {
String primaryPackage = "primary";
String secondaryPackage = "secondary";
@@ -620,6 +656,8 @@
secondaryPackage, "", true /* default available */, false /* fallback */,
null)};
setupWithPackages(packages);
+ // Start with the setting pointing to the secondary package
+ mTestSystemImpl.updateUserSetting(null, secondaryPackage);
int secondaryUserId = 10;
int userIdToChangePackageFor = multiUser ? secondaryUserId : TestSystemImpl.PRIMARY_USER_ID;
if (multiUser) {
@@ -629,31 +667,31 @@
setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages);
runWebViewBootPreparationOnMainSync();
- checkPreparationPhasesForPackage(primaryPackage, 1);
+ checkPreparationPhasesForPackage(secondaryPackage, 1);
boolean enabled = !(removalType == PackageRemovalType.DISABLE);
boolean installed = !(removalType == PackageRemovalType.UNINSTALL);
boolean hidden = (removalType == PackageRemovalType.HIDE);
- // Disable primary package and ensure secondary becomes used
+ // Disable secondary package and ensure primary becomes used
mTestSystemImpl.setPackageInfoForUser(userIdToChangePackageFor,
- createPackageInfo(primaryPackage, enabled /* enabled */, true /* valid */,
+ createPackageInfo(secondaryPackage, enabled /* enabled */, true /* valid */,
installed /* installed */, null /* signature */, 0 /* updateTime */,
hidden /* hidden */));
- mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage,
+ mWebViewUpdateServiceImpl.packageStateChanged(secondaryPackage,
removalType == PackageRemovalType.DISABLE
? WebViewUpdateService.PACKAGE_CHANGED : WebViewUpdateService.PACKAGE_REMOVED,
userIdToChangePackageFor); // USER ID
- checkPreparationPhasesForPackage(secondaryPackage, 1);
+ checkPreparationPhasesForPackage(primaryPackage, 1);
- // Again enable primary package and verify primary is used
+ // Again enable secondary package and verify secondary is used
mTestSystemImpl.setPackageInfoForUser(userIdToChangePackageFor,
- createPackageInfo(primaryPackage, true /* enabled */, true /* valid */,
+ createPackageInfo(secondaryPackage, true /* enabled */, true /* valid */,
true /* installed */));
- mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage,
+ mWebViewUpdateServiceImpl.packageStateChanged(secondaryPackage,
removalType == PackageRemovalType.DISABLE
? WebViewUpdateService.PACKAGE_CHANGED : WebViewUpdateService.PACKAGE_ADDED,
userIdToChangePackageFor);
- checkPreparationPhasesForPackage(primaryPackage, 2);
+ checkPreparationPhasesForPackage(secondaryPackage, 2);
}
/**
@@ -671,18 +709,20 @@
secondaryPackage, "", true /* default available */, false /* fallback */,
null)};
setupWithPackages(packages);
+ // Start with the setting pointing to the secondary package
+ mTestSystemImpl.updateUserSetting(null, secondaryPackage);
setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages);
int newUser = 100;
mTestSystemImpl.addUser(newUser);
- // Let the primary package be uninstalled for the new user
- mTestSystemImpl.setPackageInfoForUser(newUser,
- createPackageInfo(primaryPackage, true /* enabled */, true /* valid */,
- false /* installed */));
+ // Let the secondary package be uninstalled for the new user
mTestSystemImpl.setPackageInfoForUser(newUser,
createPackageInfo(secondaryPackage, true /* enabled */, true /* valid */,
+ false /* installed */));
+ mTestSystemImpl.setPackageInfoForUser(newUser,
+ createPackageInfo(primaryPackage, true /* enabled */, true /* valid */,
true /* installed */));
mWebViewUpdateServiceImpl.handleNewUser(newUser);
- checkPreparationPhasesForPackage(secondaryPackage, 1 /* numRelros */);
+ checkPreparationPhasesForPackage(primaryPackage, 1 /* numRelros */);
}
/**
@@ -780,9 +820,9 @@
String chosenPackage = "chosenPackage";
String nonChosenPackage = "non-chosenPackage";
WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
- new WebViewProviderInfo(chosenPackage, "", true /* default available */,
- false /* fallback */, null),
new WebViewProviderInfo(nonChosenPackage, "", true /* default available */,
+ false /* fallback */, null),
+ new WebViewProviderInfo(chosenPackage, "", true /* default available */,
false /* fallback */, null)};
setupWithPackages(packages);
@@ -810,6 +850,9 @@
}
@Test
+ @RequiresFlagsDisabled("android.webkit.update_service_v2")
+ // If the flag is set, we don't automitally switch to second package unless it is chosen
+ // directly.
public void testRecoverFailedListingWebViewPackagesAddedPackage() {
checkRecoverAfterFailListingWebviewPackages(false);
}
@@ -874,22 +917,22 @@
false /* fallback */, null),
new WebViewProviderInfo(secondPackage, "", true /* default available */,
false /* fallback */, null)};
- checkCertainPackageUsedAfterWebViewBootPreparation(firstPackage, packages);
+ checkCertainPackageUsedAfterWebViewBootPreparation(secondPackage, packages, secondPackage);
// Replace or remove the current webview package
if (replaced) {
mTestSystemImpl.setPackageInfo(
- createPackageInfo(firstPackage, true /* enabled */, false /* valid */,
+ createPackageInfo(secondPackage, true /* enabled */, false /* valid */,
true /* installed */));
- mWebViewUpdateServiceImpl.packageStateChanged(firstPackage,
+ mWebViewUpdateServiceImpl.packageStateChanged(secondPackage,
WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID);
} else {
- mTestSystemImpl.removePackageInfo(firstPackage);
- mWebViewUpdateServiceImpl.packageStateChanged(firstPackage,
+ mTestSystemImpl.removePackageInfo(secondPackage);
+ mWebViewUpdateServiceImpl.packageStateChanged(secondPackage,
WebViewUpdateService.PACKAGE_REMOVED, TestSystemImpl.PRIMARY_USER_ID);
}
- checkPreparationPhasesForPackage(secondPackage, 1);
+ checkPreparationPhasesForPackage(firstPackage, 1);
Mockito.verify(mTestSystemImpl, Mockito.never()).killPackageDependents(
Mockito.anyObject());
@@ -1073,10 +1116,12 @@
}
/**
- * Ensure that the update service does use an uninstalled package when that is the only
+ * Ensure that the update service does not use an uninstalled package even if it is the only
* package available.
*/
@Test
+ @RequiresFlagsDisabled("android.webkit.update_service_v2")
+ // If the flag is set, we return the package even if it is not installed.
public void testWithSingleUninstalledPackage() {
String testPackageName = "test.package.name";
WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
@@ -1115,12 +1160,14 @@
String installedPackage = "installedPackage";
String uninstalledPackage = "uninstalledPackage";
WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
- new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
- false /* fallback */, null),
new WebViewProviderInfo(installedPackage, "", true /* available by default */,
+ false /* fallback */, null),
+ new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
false /* fallback */, null)};
setupWithPackages(webviewPackages);
+ // Start with the setting pointing to the uninstalled package
+ mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
int secondaryUserId = 5;
if (multiUser) {
mTestSystemImpl.addUser(secondaryUserId);
@@ -1128,7 +1175,7 @@
setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, webviewPackages);
mTestSystemImpl.setPackageInfoForUser(secondaryUserId, createPackageInfo(
installedPackage, true /* enabled */, true /* valid */, true /* installed */));
- // Hide or uninstall the primary package for the second user
+ // Hide or uninstall the secondary package for the second user
mTestSystemImpl.setPackageInfo(createPackageInfo(uninstalledPackage, true /* enabled */,
true /* valid */, (testUninstalled ? false : true) /* installed */,
null /* signatures */, 0 /* updateTime */, (testHidden ? true : false)));
@@ -1166,12 +1213,14 @@
String installedPackage = "installedPackage";
String uninstalledPackage = "uninstalledPackage";
WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
- new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
- false /* fallback */, null),
new WebViewProviderInfo(installedPackage, "", true /* available by default */,
+ false /* fallback */, null),
+ new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
false /* fallback */, null)};
setupWithPackages(webviewPackages);
+ // Start with the setting pointing to the uninstalled package
+ mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
int secondaryUserId = 412;
mTestSystemImpl.addUser(secondaryUserId);
@@ -1221,12 +1270,14 @@
String installedPackage = "installedPackage";
String uninstalledPackage = "uninstalledPackage";
WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
- new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
- false /* fallback */, null),
new WebViewProviderInfo(installedPackage, "", true /* available by default */,
+ false /* fallback */, null),
+ new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
false /* fallback */, null)};
setupWithPackages(webviewPackages);
+ // Start with the setting pointing to the uninstalled package
+ mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
int secondaryUserId = 4;
mTestSystemImpl.addUser(secondaryUserId);
@@ -1433,11 +1484,16 @@
new WebViewProviderInfo(newSdkPackage.packageName, "", true, false, null);
WebViewProviderInfo currentSdkProviderInfo =
new WebViewProviderInfo(currentSdkPackage.packageName, "", true, false, null);
- WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
- new WebViewProviderInfo(oldSdkPackage.packageName, "", true, false, null),
- currentSdkProviderInfo, newSdkProviderInfo};
+ WebViewProviderInfo[] packages =
+ new WebViewProviderInfo[] {
+ currentSdkProviderInfo,
+ new WebViewProviderInfo(oldSdkPackage.packageName, "", true, false, null),
+ newSdkProviderInfo
+ };
setupWithPackages(packages);
-;
+ // Start with the setting pointing to the invalid package
+ mTestSystemImpl.updateUserSetting(null, oldSdkPackage.packageName);
+
mTestSystemImpl.setPackageInfo(newSdkPackage);
mTestSystemImpl.setPackageInfo(currentSdkPackage);
mTestSystemImpl.setPackageInfo(oldSdkPackage);
@@ -1467,4 +1523,74 @@
assertEquals(
defaultPackage1, mWebViewUpdateServiceImpl.getDefaultWebViewPackage().packageName);
}
+
+ @Test
+ @RequiresFlagsEnabled("android.webkit.update_service_v2")
+ public void testDefaultWebViewPackageEnabling() {
+ String testPackage = "testDefault";
+ WebViewProviderInfo[] packages =
+ new WebViewProviderInfo[] {
+ new WebViewProviderInfo(
+ testPackage,
+ "",
+ true /* default available */,
+ false /* fallback */,
+ null)
+ };
+ setupWithPackages(packages);
+ mTestSystemImpl.setPackageInfo(
+ createPackageInfo(
+ testPackage, false /* enabled */, true /* valid */, true /* installed */));
+
+ // Check that the boot time logic re-enables the default package.
+ runWebViewBootPreparationOnMainSync();
+ Mockito.verify(mTestSystemImpl)
+ .enablePackageForAllUsers(
+ Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true));
+ }
+
+ private void testDefaultPackageChosen(PackageInfo packageInfo) {
+ WebViewProviderInfo[] packages =
+ new WebViewProviderInfo[] {
+ new WebViewProviderInfo(packageInfo.packageName, "", true, false, null)
+ };
+ setupWithPackages(packages);
+ mTestSystemImpl.setPackageInfo(packageInfo);
+
+ runWebViewBootPreparationOnMainSync();
+ mWebViewUpdateServiceImpl.notifyRelroCreationCompleted();
+
+ assertEquals(
+ packageInfo.packageName,
+ mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName);
+
+ WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
+ assertEquals(packageInfo.packageName, response.packageInfo.packageName);
+ }
+
+ @Test
+ @RequiresFlagsEnabled("android.webkit.update_service_v2")
+ public void testDisabledDefaultPackageChosen() {
+ PackageInfo disabledPackage =
+ createPackageInfo(
+ "disabledPackage",
+ false /* enabled */,
+ true /* valid */,
+ true /* installed */);
+
+ testDefaultPackageChosen(disabledPackage);
+ }
+
+ @Test
+ @RequiresFlagsEnabled("android.webkit.update_service_v2")
+ public void testUninstalledDefaultPackageChosen() {
+ PackageInfo uninstalledPackage =
+ createPackageInfo(
+ "uninstalledPackage",
+ true /* enabled */,
+ true /* valid */,
+ false /* installed */);
+
+ testDefaultPackageChosen(uninstalledPackage);
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index c1f35cc..723ac15 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -13792,8 +13792,7 @@
NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
mBinderService.setNotificationPolicy("package", policy, false);
- verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy),
- eq(ZenModeConfig.UPDATE_ORIGIN_APP));
+ verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy));
}
@Test
@@ -13859,7 +13858,7 @@
verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt());
} else {
verify(zenModeHelper).applyGlobalPolicyAsImplicitZenRule(anyString(), anyInt(),
- eq(policy), anyInt());
+ eq(policy));
}
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
index 08af09c..0e20daf 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
@@ -143,12 +143,12 @@
Policy.policyState(false, true), 0);
ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
- assertThat(zenPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+ assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
Policy notAllowed = new Policy(0, 0, 0, 0,
Policy.policyState(false, false), 0);
ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed);
- assertThat(zenPolicyNotAllowed.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
+ assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
}
@Test
@@ -158,12 +158,11 @@
Policy.policyState(false, true), 0);
ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
- assertThat(zenPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET);
+ assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
Policy notAllowed = new Policy(0, 0, 0, 0,
Policy.policyState(false, false), 0);
ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed);
- assertThat(zenPolicyNotAllowed.getAllowedChannels())
- .isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET);
+ assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
}
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
index 3d8ec2e..f604f1e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
@@ -52,7 +52,6 @@
.setShouldMaximizeDoze(true)
.setShouldUseNightMode(false)
.setShouldSuppressAmbientDisplay(false).setShouldSuppressAmbientDisplay(true)
- .setUserModifiedFields(8)
.build();
assertThat(deviceEffects.shouldDimWallpaper()).isTrue();
@@ -65,7 +64,6 @@
assertThat(deviceEffects.shouldMinimizeRadioUsage()).isFalse();
assertThat(deviceEffects.shouldUseNightMode()).isFalse();
assertThat(deviceEffects.shouldSuppressAmbientDisplay()).isTrue();
- assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(8);
}
@Test
@@ -97,7 +95,6 @@
.setShouldMinimizeRadioUsage(true)
.setShouldUseNightMode(true)
.setShouldSuppressAmbientDisplay(true)
- .setUserModifiedFields(6)
.build();
Parcel parcel = Parcel.obtain();
@@ -116,7 +113,6 @@
assertThat(copy.shouldUseNightMode()).isTrue();
assertThat(copy.shouldSuppressAmbientDisplay()).isTrue();
assertThat(copy.shouldDisplayGrayscale()).isFalse();
- assertThat(copy.getUserModifiedFields()).isEqualTo(6);
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index dd252f3..e523e79f 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -164,7 +164,7 @@
.allowConversations(CONVERSATION_SENDERS_IMPORTANT)
.showLights(false)
.showInAmbientDisplay(false)
- .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+ .allowPriorityChannels(false)
.build();
Policy originalPolicy = config.toNotificationPolicy();
@@ -255,7 +255,7 @@
.allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
.allowMessages(ZenPolicy.PEOPLE_TYPE_STARRED)
.allowConversations(ZenPolicy.CONVERSATION_SENDERS_NONE)
- .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+ .allowPriorityChannels(false)
.build();
ZenModeConfig config = getMutedAllConfig();
@@ -284,8 +284,7 @@
actual.getPriorityConversationSenders());
assertEquals(expected.getPriorityCallSenders(), actual.getPriorityCallSenders());
assertEquals(expected.getPriorityMessageSenders(), actual.getPriorityMessageSenders());
- assertEquals(expected.getAllowedChannels(), actual.getAllowedChannels());
- assertEquals(expected.getUserModifiedFields(), actual.getUserModifiedFields());
+ assertEquals(expected.getPriorityChannels(), actual.getPriorityChannels());
}
@Test
@@ -342,45 +341,32 @@
ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
rule.zenPolicy = null;
rule.zenDeviceEffects = null;
-
assertThat(rule.canBeUpdatedByApp()).isTrue();
rule.userModifiedFields = 1;
+
assertThat(rule.canBeUpdatedByApp()).isFalse();
}
@Test
public void testCanBeUpdatedByApp_policyModified() throws Exception {
- ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder().setUserModifiedFields(0);
- ZenPolicy policy = policyBuilder.build();
-
ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
- rule.zenPolicy = policy;
-
- assertThat(rule.userModifiedFields).isEqualTo(0);
+ rule.zenPolicy = new ZenPolicy();
assertThat(rule.canBeUpdatedByApp()).isTrue();
- policy = policyBuilder.setUserModifiedFields(1).build();
- assertThat(policy.getUserModifiedFields()).isEqualTo(1);
- rule.zenPolicy = policy;
+ rule.zenPolicyUserModifiedFields = 1;
+
assertThat(rule.canBeUpdatedByApp()).isFalse();
}
@Test
public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception {
- ZenDeviceEffects.Builder deviceEffectsBuilder =
- new ZenDeviceEffects.Builder().setUserModifiedFields(0);
- ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build();
-
ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
- rule.zenDeviceEffects = deviceEffects;
-
- assertThat(rule.userModifiedFields).isEqualTo(0);
+ rule.zenDeviceEffects = new ZenDeviceEffects.Builder().build();
assertThat(rule.canBeUpdatedByApp()).isTrue();
- deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build();
- assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1);
- rule.zenDeviceEffects = deviceEffects;
+ rule.zenDeviceEffectsUserModifiedFields = 1;
+
assertThat(rule.canBeUpdatedByApp()).isFalse();
}
@@ -406,6 +392,8 @@
rule.allowManualInvocation = ALLOW_MANUAL;
rule.type = TYPE;
rule.userModifiedFields = 16;
+ rule.zenPolicyUserModifiedFields = 5;
+ rule.zenDeviceEffectsUserModifiedFields = 2;
rule.iconResName = ICON_RES_NAME;
rule.triggerDescription = TRIGGER_DESC;
rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
@@ -432,6 +420,9 @@
assertEquals(rule.iconResName, parceled.iconResName);
assertEquals(rule.type, parceled.type);
assertEquals(rule.userModifiedFields, parceled.userModifiedFields);
+ assertEquals(rule.zenPolicyUserModifiedFields, parceled.zenPolicyUserModifiedFields);
+ assertEquals(rule.zenDeviceEffectsUserModifiedFields,
+ parceled.zenDeviceEffectsUserModifiedFields);
assertEquals(rule.triggerDescription, parceled.triggerDescription);
assertEquals(rule.zenPolicy, parceled.zenPolicy);
assertEquals(rule.deletionInstant, parceled.deletionInstant);
@@ -511,6 +502,8 @@
rule.allowManualInvocation = ALLOW_MANUAL;
rule.type = TYPE;
rule.userModifiedFields = 4;
+ rule.zenPolicyUserModifiedFields = 5;
+ rule.zenDeviceEffectsUserModifiedFields = 2;
rule.iconResName = ICON_RES_NAME;
rule.triggerDescription = TRIGGER_DESC;
rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
@@ -541,6 +534,9 @@
assertEquals(rule.allowManualInvocation, fromXml.allowManualInvocation);
assertEquals(rule.type, fromXml.type);
assertEquals(rule.userModifiedFields, fromXml.userModifiedFields);
+ assertEquals(rule.zenPolicyUserModifiedFields, fromXml.zenPolicyUserModifiedFields);
+ assertEquals(rule.zenDeviceEffectsUserModifiedFields,
+ fromXml.zenDeviceEffectsUserModifiedFields);
assertEquals(rule.triggerDescription, fromXml.triggerDescription);
assertEquals(rule.iconResName, fromXml.iconResName);
assertEquals(rule.deletionInstant, fromXml.deletionInstant);
@@ -694,10 +690,9 @@
.allowSystem(true)
.allowReminders(false)
.allowEvents(true)
- .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+ .allowPriorityChannels(false)
.hideAllVisualEffects()
.showVisualEffect(ZenPolicy.VISUAL_EFFECT_AMBIENT, true)
- .setUserModifiedFields(4)
.build();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -721,7 +716,7 @@
assertEquals(policy.getPriorityCategorySystem(), fromXml.getPriorityCategorySystem());
assertEquals(policy.getPriorityCategoryReminders(), fromXml.getPriorityCategoryReminders());
assertEquals(policy.getPriorityCategoryEvents(), fromXml.getPriorityCategoryEvents());
- assertEquals(policy.getAllowedChannels(), fromXml.getAllowedChannels());
+ assertEquals(policy.getPriorityChannels(), fromXml.getPriorityChannels());
assertEquals(policy.getVisualEffectFullScreenIntent(),
fromXml.getVisualEffectFullScreenIntent());
@@ -732,7 +727,6 @@
assertEquals(policy.getVisualEffectAmbient(), fromXml.getVisualEffectAmbient());
assertEquals(policy.getVisualEffectNotificationList(),
fromXml.getVisualEffectNotificationList());
- assertEquals(policy.getUserModifiedFields(), fromXml.getUserModifiedFields());
}
private ZenModeConfig getMutedRingerConfig() {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index 9d7cf53..2e64645 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -73,13 +73,15 @@
: Set.of("version", "manualRule", "automaticRules");
// Differences for flagged fields are only generated if the flag is enabled.
- // "Metadata" fields (userModifiedFields, deletionInstant) are not compared.
+ // "Metadata" fields (userModifiedFields & co, deletionInstant) are not compared.
private static final Set<String> ZEN_RULE_EXEMPT_FIELDS =
android.app.Flags.modesApi()
- ? Set.of("userModifiedFields", "deletionInstant")
+ ? Set.of("userModifiedFields", "zenPolicyUserModifiedFields",
+ "zenDeviceEffectsUserModifiedFields", "deletionInstant")
: Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, "userModifiedFields",
+ "zenPolicyUserModifiedFields", "zenDeviceEffectsUserModifiedFields",
"deletionInstant");
// allowPriorityChannels is flagged by android.app.modes_api
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
index 29208f4..7d6e12c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
@@ -546,13 +546,13 @@
// Create a policy to allow channels through, which means shouldIntercept is false
ZenModeConfig config = new ZenModeConfig();
Policy policy = config.toNotificationPolicy(new ZenPolicy.Builder()
- .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+ .allowPriorityChannels(true)
.build());
assertFalse(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r));
// Now create a policy which does not allow priority channels:
policy = config.toNotificationPolicy(new ZenPolicy.Builder()
- .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+ .allowPriorityChannels(false)
.build());
assertTrue(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r));
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 9e3e336..edc876a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -46,6 +46,7 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.provider.Settings.Global.ZEN_MODE_ALARMS;
import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+import static android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS;
import static android.provider.Settings.Global.ZEN_MODE_OFF;
import static android.service.notification.Condition.SOURCE_SCHEDULE;
import static android.service.notification.Condition.SOURCE_USER_ACTION;
@@ -67,6 +68,7 @@
import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW;
import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
+import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
@@ -295,6 +297,8 @@
when(appInfoSpy.loadLabel(any())).thenReturn(CUSTOM_APP_LABEL);
when(mPackageManager.getApplicationInfo(eq(CUSTOM_PKG_NAME), anyInt()))
.thenReturn(appInfoSpy);
+ when(mPackageManager.getApplicationInfo(eq(mContext.getPackageName()), anyInt()))
+ .thenReturn(appInfoSpy);
mZenModeHelper.mPm = mPackageManager;
mZenModeEventLogger.reset();
@@ -1151,7 +1155,7 @@
.allowAlarms(true)
.allowRepeatCallers(false)
.allowCalls(PEOPLE_TYPE_STARRED)
- .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+ .allowPriorityChannels(false)
.build();
mZenModeHelper.mConfig.automaticRules.put(rule.id, rule);
List<StatsEvent> events = new LinkedList<>();
@@ -1174,7 +1178,7 @@
assertThat(policy.getAllowCallsFrom().getNumber())
.isEqualTo(DNDProtoEnums.PEOPLE_STARRED);
assertThat(policy.getAllowChannels().getNumber())
- .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_NONE);
+ .isEqualTo(DNDProtoEnums.CHANNEL_POLICY_NONE);
}
}
assertTrue("couldn't find custom rule", foundCustomEvent);
@@ -2236,12 +2240,7 @@
AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
- // savedRule.getDeviceEffects() is equal to zde, except for the userModifiedFields.
- // So we clear before comparing.
- ZenDeviceEffects savedEffects = new ZenDeviceEffects.Builder(savedRule.getDeviceEffects())
- .setUserModifiedFields(0).build();
-
- assertThat(savedEffects).isEqualTo(zde);
+ assertThat(savedRule.getDeviceEffects()).isEqualTo(zde);
}
@Test
@@ -2331,12 +2330,7 @@
AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
- // savedRule.getDeviceEffects() is equal to updateFromUser, except for the
- // userModifiedFields, so we clear before comparing.
- ZenDeviceEffects savedEffects = new ZenDeviceEffects.Builder(savedRule.getDeviceEffects())
- .setUserModifiedFields(0).build();
-
- assertThat(savedEffects).isEqualTo(updateFromUser);
+ assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser);
}
@Test
@@ -3098,7 +3092,7 @@
DNDPolicyProto origDndProto = mZenModeEventLogger.getPolicyProto(0);
checkDndProtoMatchesSetupZenConfig(origDndProto);
assertThat(origDndProto.getAllowChannels().getNumber())
- .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_PRIORITY);
+ .isEqualTo(DNDProtoEnums.CHANNEL_POLICY_PRIORITY);
// Second message where we change the policy:
// - DND_POLICY_CHANGED (indicates only the policy changed and nothing else)
@@ -3110,7 +3104,7 @@
.isEqualTo(DNDProtoEnums.UNKNOWN_RULE);
DNDPolicyProto dndProto = mZenModeEventLogger.getPolicyProto(1);
assertThat(dndProto.getAllowChannels().getNumber())
- .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_NONE);
+ .isEqualTo(DNDProtoEnums.CHANNEL_POLICY_NONE);
}
@Test
@@ -3299,7 +3293,7 @@
// one rule, custom policy, allows channels
ZenPolicy customPolicy = new ZenPolicy.Builder()
- .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+ .allowPriorityChannels(true)
.build();
AutomaticZenRule zenRule = new AutomaticZenRule("name",
@@ -3321,7 +3315,7 @@
// add new rule with policy that disallows channels
ZenPolicy strictPolicy = new ZenPolicy.Builder()
- .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE)
+ .allowPriorityChannels(false)
.build();
AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
@@ -3411,7 +3405,6 @@
rule.allowManualInvocation = ALLOW_MANUAL;
rule.type = TYPE;
- rule.userModifiedFields = AutomaticZenRule.FIELD_NAME;
rule.iconResName = ICON_RES_NAME;
rule.triggerDescription = TRIGGER_DESC;
@@ -3426,7 +3419,6 @@
assertEquals(POLICY, actual.getZenPolicy());
assertEquals(CONFIG_ACTIVITY, actual.getConfigurationActivity());
assertEquals(TYPE, actual.getType());
- assertEquals(AutomaticZenRule.FIELD_NAME, actual.getUserModifiedFields());
assertEquals(ALLOW_MANUAL, actual.isManualInvocationAllowed());
assertEquals(CREATION_TIME, actual.getCreationTime());
assertEquals(OWNER.getPackageName(), actual.getPackageName());
@@ -3453,29 +3445,31 @@
.setManualInvocationAllowed(ALLOW_MANUAL)
.build();
- ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(OWNER.getPackageName(), azr,
+ UPDATE_ORIGIN_APP, "add", CUSTOM_PKG_UID);
- mZenModeHelper.populateZenRule(OWNER.getPackageName(), azr, rule, UPDATE_ORIGIN_APP, true);
+ ZenModeConfig.ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
- assertEquals(NAME, rule.name);
- assertEquals(OWNER, rule.component);
- assertEquals(CONDITION_ID, rule.conditionId);
- assertEquals(INTERRUPTION_FILTER_ZR, rule.zenMode);
- assertEquals(ENABLED, rule.enabled);
- assertEquals(POLICY, rule.zenPolicy);
- assertEquals(CONFIG_ACTIVITY, rule.configurationActivity);
- assertEquals(TYPE, rule.type);
- assertEquals(ALLOW_MANUAL, rule.allowManualInvocation);
- assertEquals(OWNER.getPackageName(), rule.getPkg());
- assertEquals(ICON_RES_NAME, rule.iconResName);
+ assertThat(storedRule).isNotNull();
+ assertEquals(NAME, storedRule.name);
+ assertEquals(OWNER, storedRule.component);
+ assertEquals(CONDITION_ID, storedRule.conditionId);
+ assertEquals(INTERRUPTION_FILTER_ZR, storedRule.zenMode);
+ assertEquals(ENABLED, storedRule.enabled);
+ assertEquals(POLICY, storedRule.zenPolicy);
+ assertEquals(CONFIG_ACTIVITY, storedRule.configurationActivity);
+ assertEquals(TYPE, storedRule.type);
+ assertEquals(ALLOW_MANUAL, storedRule.allowManualInvocation);
+ assertEquals(OWNER.getPackageName(), storedRule.getPkg());
+ assertEquals(ICON_RES_NAME, storedRule.iconResName);
// Because the origin of the update is the app, we don't expect the bitmask to change.
- assertEquals(0, rule.userModifiedFields);
- assertEquals(TRIGGER_DESC, rule.triggerDescription);
+ assertEquals(0, storedRule.userModifiedFields);
+ assertEquals(TRIGGER_DESC, storedRule.triggerDescription);
}
@Test
@EnableFlags(Flags.FLAG_MODES_API)
- public void automaticZenRuleToZenRule_updatesNameUnlessUserModified() {
+ public void updateAutomaticZenRule_fromApp_updatesNameUnlessUserModified() {
// Add a starting rule with the name OriginalName.
AutomaticZenRule azrBase = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
@@ -3492,7 +3486,6 @@
Process.SYSTEM_UID);
rule = mZenModeHelper.getAutomaticZenRule(ruleId);
assertThat(rule.getName()).isEqualTo("NewName");
- assertThat(rule.canUpdate()).isTrue();
// The user modifies some other field in the rule, which makes the rule as a whole not
// app modifiable.
@@ -3501,10 +3494,6 @@
.build();
mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason",
Process.SYSTEM_UID);
- rule = mZenModeHelper.getAutomaticZenRule(ruleId);
- assertThat(rule.getUserModifiedFields())
- .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
- assertThat(rule.canUpdate()).isFalse();
// ...but the app can still modify the name, because the name itself hasn't been modified
// by the user.
@@ -3524,8 +3513,6 @@
Process.SYSTEM_UID);
rule = mZenModeHelper.getAutomaticZenRule(ruleId);
assertThat(rule.getName()).isEqualTo("UserProvidedName");
- assertThat(rule.getUserModifiedFields()).isEqualTo(AutomaticZenRule.FIELD_NAME
- | AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
// The app is no longer able to modify the name.
azrUpdate = new AutomaticZenRule.Builder(rule)
@@ -3539,7 +3526,7 @@
@Test
@EnableFlags(Flags.FLAG_MODES_API)
- public void automaticZenRuleToZenRule_updatesBitmaskAndValueForUserOrigin() {
+ public void updateAutomaticZenRule_fromUser_updatesBitmaskAndValue() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
.setZenPolicy(new ZenPolicy.Builder().build())
@@ -3552,7 +3539,7 @@
// Modifies the zen policy and device effects
ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
- .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+ .allowPriorityChannels(true)
.build();
ZenDeviceEffects deviceEffects =
new ZenDeviceEffects.Builder(rule.getDeviceEffects())
@@ -3571,85 +3558,21 @@
// UPDATE_ORIGIN_USER should change the bitmask and change the values.
assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
- assertThat(rule.getUserModifiedFields())
- .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
- assertThat(rule.getZenPolicy().getUserModifiedFields())
- .isEqualTo(ZenPolicy.FIELD_ALLOW_CHANNELS);
- assertThat(rule.getZenPolicy().getAllowedChannels())
- .isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
- assertThat(rule.getDeviceEffects().getUserModifiedFields())
- .isEqualTo(ZenDeviceEffects.FIELD_GRAYSCALE);
+ assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+
+ ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(storedRule.userModifiedFields)
+ .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
+ assertThat(storedRule.zenPolicyUserModifiedFields)
+ .isEqualTo(ZenPolicy.FIELD_ALLOW_CHANNELS);
+ assertThat(storedRule.zenDeviceEffectsUserModifiedFields)
+ .isEqualTo(ZenDeviceEffects.FIELD_GRAYSCALE);
}
@Test
@EnableFlags(Flags.FLAG_MODES_API)
- public void automaticZenRuleToZenRule_doesNotUpdateValuesForInitUserOrigin() {
- // Adds a starting rule with empty zen policies and device effects
- AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
- .setInterruptionFilter(INTERRUPTION_FILTER_ALL) // Already the default, no change
- .setZenPolicy(new ZenPolicy.Builder()
- .allowReminders(false)
- .build())
- .setDeviceEffects(new ZenDeviceEffects.Builder()
- .setShouldDisplayGrayscale(false)
- .build())
- .build();
- // Adds the rule using the user, to set user-modified bits.
- String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
- azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID);
- AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
- assertThat(rule.canUpdate()).isFalse();
- assertThat(rule.getUserModifiedFields()).isEqualTo(AutomaticZenRule.FIELD_NAME);
-
- ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
- .allowReminders(true)
- .build();
- ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder(rule.getDeviceEffects())
- .setShouldDisplayGrayscale(true)
- .build();
- AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
- .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
- .setZenPolicy(policy)
- .setDeviceEffects(deviceEffects)
- .build();
-
- // Attempts to update the rule with the AZR from origin init user.
- mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_INIT_USER, "reason",
- Process.SYSTEM_UID);
- AutomaticZenRule unchangedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
-
- // UPDATE_ORIGIN_INIT_USER does not change the bitmask or values if rule is user modified.
- // TODO: b/318506692 - Remove once we check that INIT origins can't call add/updateAZR.
- assertThat(unchangedRule.getUserModifiedFields()).isEqualTo(rule.getUserModifiedFields());
- assertThat(unchangedRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALL);
- assertThat(unchangedRule.getZenPolicy().getUserModifiedFields()).isEqualTo(
- rule.getZenPolicy().getUserModifiedFields());
- assertThat(unchangedRule.getZenPolicy().getPriorityCategoryReminders()).isEqualTo(
- ZenPolicy.STATE_DISALLOW);
- assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(
- rule.getDeviceEffects().getUserModifiedFields());
- assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isFalse();
-
- // Creates a new rule with the AZR from origin init user.
- String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
- azrUpdate, UPDATE_ORIGIN_INIT_USER, "reason", Process.SYSTEM_UID);
- AutomaticZenRule newRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
-
- // UPDATE_ORIGIN_INIT_USER does change the values if the rule is new,
- // but does not update the bitmask.
- assertThat(newRule.getUserModifiedFields()).isEqualTo(0);
- assertThat(newRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
- assertThat(newRule.getZenPolicy().getUserModifiedFields()).isEqualTo(0);
- assertThat(newRule.getZenPolicy().getPriorityCategoryReminders())
- .isEqualTo(ZenPolicy.STATE_ALLOW);
- assertThat(newRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(0);
- assertThat(newRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
- }
-
- @Test
- @EnableFlags(Flags.FLAG_MODES_API)
- public void automaticZenRuleToZenRule_updatesValuesForSystemUiOrigin() {
+ public void updateAutomaticZenRule_fromSystemUi_updatesValues() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
.setInterruptionFilter(INTERRUPTION_FILTER_ALL)
@@ -3685,17 +3608,19 @@
rule = mZenModeHelper.getAutomaticZenRule(ruleId);
// UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI should change the value but NOT update the bitmask.
- assertThat(rule.getUserModifiedFields()).isEqualTo(0);
- assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo(0);
assertThat(rule.getZenPolicy().getPriorityCategoryReminders())
.isEqualTo(ZenPolicy.STATE_ALLOW);
- assertThat(rule.getDeviceEffects().getUserModifiedFields()).isEqualTo(0);
assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+
+ ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(storedRule.userModifiedFields).isEqualTo(0);
+ assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(0);
+ assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(0);
}
@Test
@EnableFlags(Flags.FLAG_MODES_API)
- public void automaticZenRuleToZenRule_updatesValuesIfRuleNotUserModified() {
+ public void updateAutomaticZenRule_fromApp_updatesValuesIfRuleNotUserModified() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
.setInterruptionFilter(INTERRUPTION_FILTER_ALL)
@@ -3710,7 +3635,6 @@
String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
- assertThat(rule.canUpdate()).isTrue();
ZenPolicy policy = new ZenPolicy.Builder()
.allowReminders(true)
@@ -3718,57 +3642,59 @@
ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
.setShouldDisplayGrayscale(true)
.build();
- AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
+ AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
.setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
.setZenPolicy(policy)
.setDeviceEffects(deviceEffects)
.build();
- // Since the rule is not already user modified, UPDATE_ORIGIN_UNKNOWN can modify the rule.
+ // Since the rule is not already user modified, UPDATE_ORIGIN_APP can modify the rule.
// The bitmask is not modified.
- mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_UNKNOWN, "reason",
+ mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason",
Process.SYSTEM_UID);
- AutomaticZenRule unchangedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
- assertThat(unchangedRule.getUserModifiedFields()).isEqualTo(rule.getUserModifiedFields());
- assertThat(unchangedRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
- assertThat(unchangedRule.getZenPolicy().getUserModifiedFields()).isEqualTo(
- rule.getZenPolicy().getUserModifiedFields());
- assertThat(unchangedRule.getZenPolicy().getPriorityCategoryReminders())
+ ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(storedRule.userModifiedFields).isEqualTo(0);
+
+ assertThat(storedRule.zenMode).isEqualTo(ZEN_MODE_ALARMS);
+ assertThat(storedRule.zenPolicy.getPriorityCategoryReminders())
.isEqualTo(ZenPolicy.STATE_ALLOW);
- assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(
- rule.getDeviceEffects().getUserModifiedFields());
- assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+ assertThat(storedRule.zenDeviceEffects.shouldDisplayGrayscale()).isTrue();
+ assertThat(storedRule.userModifiedFields).isEqualTo(0);
+ assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(0);
+ assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(0);
// Creates another rule, this time from user. This will have user modified bits set.
String ruleIdUser = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID);
- AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser);
- assertThat(ruleUser.canUpdate()).isFalse();
+ storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleIdUser);
+ int ruleModifiedFields = storedRule.userModifiedFields;
+ int rulePolicyModifiedFields = storedRule.zenPolicyUserModifiedFields;
+ int ruleDeviceEffectsModifiedFields = storedRule.zenDeviceEffectsUserModifiedFields;
- // Zen rule update coming from unknown origin. This cannot fully update the rule, because
+ // Zen rule update coming from the app again. This cannot fully update the rule, because
// the rule is already considered user modified.
- mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, UPDATE_ORIGIN_UNKNOWN,
+ mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, UPDATE_ORIGIN_APP,
"reason", Process.SYSTEM_UID);
- ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser);
+ AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser);
- // UPDATE_ORIGIN_UNKNOWN can only change the value if the rule is not already user modified,
+ // The app can only change the value if the rule is not already user modified,
// so the rule is not changed, and neither is the bitmask.
assertThat(ruleUser.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALL);
- // Interruption Filter All is the default value, so it's not included as a modified field.
- assertThat(ruleUser.getUserModifiedFields() | AutomaticZenRule.FIELD_NAME).isGreaterThan(0);
- assertThat(ruleUser.getZenPolicy().getUserModifiedFields()
- | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS).isGreaterThan(0);
assertThat(ruleUser.getZenPolicy().getPriorityCategoryReminders())
.isEqualTo(ZenPolicy.STATE_DISALLOW);
- assertThat(ruleUser.getDeviceEffects().getUserModifiedFields()
- | ZenDeviceEffects.FIELD_GRAYSCALE).isGreaterThan(0);
assertThat(ruleUser.getDeviceEffects().shouldDisplayGrayscale()).isFalse();
+
+ storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleIdUser);
+ assertThat(storedRule.userModifiedFields).isEqualTo(ruleModifiedFields);
+ assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(rulePolicyModifiedFields);
+ assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(
+ ruleDeviceEffectsModifiedFields);
}
@Test
@EnableFlags(Flags.FLAG_MODES_API)
- public void automaticZenRuleToZenRule_updatesValuesIfRuleNew() {
+ public void addAutomaticZenRule_updatesValues() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
.setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
@@ -3779,21 +3705,22 @@
.setShouldDisplayGrayscale(true)
.build())
.build();
- // Adds the rule using origin unknown, to show that a new rule is always allowed.
String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
- azrBase, UPDATE_ORIGIN_UNKNOWN, "reason", Process.SYSTEM_UID);
+ azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
// The values are modified but the bitmask is not.
- assertThat(rule.canUpdate()).isTrue();
assertThat(rule.getZenPolicy().getPriorityCategoryReminders())
.isEqualTo(ZenPolicy.STATE_ALLOW);
assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
+
+ ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(storedRule.canBeUpdatedByApp()).isTrue();
}
@Test
@EnableFlags(Flags.FLAG_MODES_API)
- public void automaticZenRuleToZenRule_nullDeviceEffectsUpdate() {
+ public void updateAutomaticZenRule_nullDeviceEffectsUpdate() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
.setDeviceEffects(new ZenDeviceEffects.Builder().build())
@@ -3808,9 +3735,9 @@
.setDeviceEffects(null)
.build();
- // Zen rule update coming from unknown origin, but since the rule isn't already
+ // Zen rule update coming from app, but since the rule isn't already
// user modified, it can be updated.
- mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason",
+ mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_APP, "reason",
Process.SYSTEM_UID);
rule = mZenModeHelper.getAutomaticZenRule(ruleId);
@@ -3820,7 +3747,7 @@
@Test
@EnableFlags(Flags.FLAG_MODES_API)
- public void automaticZenRuleToZenRule_nullPolicyUpdate() {
+ public void updateAutomaticZenRule_nullPolicyUpdate() {
// Adds a starting rule with empty zen policies and device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
.setZenPolicy(new ZenPolicy.Builder().build())
@@ -3829,16 +3756,15 @@
String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
- assertThat(rule.canUpdate()).isTrue();
AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase)
// Set zen policy to null
.setZenPolicy(null)
.build();
- // Zen rule update coming from unknown origin, but since the rule isn't already
+ // Zen rule update coming from app, but since the rule isn't already
// user modified, it can be updated.
- mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason",
+ mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_APP, "reason",
Process.SYSTEM_UID);
rule = mZenModeHelper.getAutomaticZenRule(ruleId);
@@ -3860,11 +3786,10 @@
String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
- assertThat(rule.canUpdate()).isTrue();
// Create a fully populated ZenPolicy.
ZenPolicy policy = new ZenPolicy.Builder()
- .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) // Differs from the default
+ .allowPriorityChannels(false) // Differs from the default
.allowReminders(true) // Differs from the default
.allowEvents(true) // Differs from the default
.allowConversations(ZenPolicy.CONVERSATION_SENDERS_IMPORTANT)
@@ -3894,9 +3819,11 @@
// New ZenPolicy differs from the default config
assertThat(rule.getZenPolicy()).isNotNull();
- assertThat(rule.getZenPolicy().getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
- assertThat(rule.canUpdate()).isFalse();
- assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo(
+ assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+
+ ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(storedRule.canBeUpdatedByApp()).isFalse();
+ assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(
ZenPolicy.FIELD_ALLOW_CHANNELS
| ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS
| ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS
@@ -3919,7 +3846,6 @@
String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID);
AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
- assertThat(rule.canUpdate()).isTrue();
ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
.setShouldDisplayGrayscale(true)
@@ -3936,8 +3862,10 @@
// New ZenDeviceEffects is used; all fields considered set, since previously were null.
assertThat(rule.getDeviceEffects()).isNotNull();
assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
- assertThat(rule.canUpdate()).isFalse();
- assertThat(rule.getDeviceEffects().getUserModifiedFields()).isEqualTo(
+
+ ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(storedRule.canBeUpdatedByApp()).isFalse();
+ assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(
ZenDeviceEffects.FIELD_GRAYSCALE);
}
@@ -4340,7 +4268,6 @@
String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000);
- assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).canUpdate()).isTrue();
// User customizes it.
AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
@@ -4372,9 +4299,11 @@
assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
assertThat(finalRule.getZenPolicy().getPriorityCategoryRepeatCallers()).isEqualTo(
ZenPolicy.STATE_ALLOW);
- assertThat(finalRule.getUserModifiedFields()).isEqualTo(
+
+ ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(storedRule.userModifiedFields).isEqualTo(
AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
- assertThat(finalRule.getZenPolicy().getUserModifiedFields()).isEqualTo(
+ assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(
ZenPolicy.FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS);
// Also, we discarded the "deleted rule" since we already used it for restoration.
@@ -4653,7 +4582,7 @@
ZEN_MODE_IMPORTANT_INTERRUPTIONS);
assertThat(mZenModeHelper.mConfig.automaticRules.values())
- .comparingElementsUsing(IGNORE_TIMESTAMPS)
+ .comparingElementsUsing(IGNORE_METADATA)
.containsExactly(
expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
null, true));
@@ -4673,12 +4602,75 @@
ZEN_MODE_ALARMS);
assertThat(mZenModeHelper.mConfig.automaticRules.values())
- .comparingElementsUsing(IGNORE_TIMESTAMPS)
+ .comparingElementsUsing(IGNORE_METADATA)
.containsExactly(
expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true));
}
@Test
+ @EnableFlags(android.app.Flags.FLAG_MODES_API)
+ public void applyGlobalZenModeAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() {
+ mZenModeHelper.mConfig.automaticRules.clear();
+ String pkg = mContext.getPackageName();
+
+ // From app, call "setInterruptionFilter" and create and implicit rule.
+ mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID,
+ ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+ String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet());
+ assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode)
+ .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+
+ // From user, update that rule's interruption filter.
+ AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason",
+ Process.SYSTEM_UID);
+
+ // From app, call "setInterruptionFilter" again.
+ mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID,
+ ZEN_MODE_NO_INTERRUPTIONS);
+
+ // The app's update was ignored, and the user's update is still current, and the current
+ // mode is the one they chose.
+ assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode)
+ .isEqualTo(ZEN_MODE_ALARMS);
+ assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_MODES_API)
+ public void applyGlobalZenModeAsImplicitZenRule_ruleCustomizedButNotFilter_updatesRule() {
+ mZenModeHelper.mConfig.automaticRules.clear();
+ String pkg = mContext.getPackageName();
+
+ // From app, call "setInterruptionFilter" and create and implicit rule.
+ mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID,
+ ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+ String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet());
+ assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode)
+ .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+
+ // From user, update something in that rule, but not the interruption filter.
+ AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule)
+ .setName("Renamed")
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason",
+ Process.SYSTEM_UID);
+
+ // From app, call "setInterruptionFilter" again.
+ mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID,
+ ZEN_MODE_NO_INTERRUPTIONS);
+
+ // The app's update was accepted, and the current mode is the one that they wanted.
+ assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode)
+ .isEqualTo(ZEN_MODE_NO_INTERRUPTIONS);
+ assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_NO_INTERRUPTIONS);
+ }
+
+ @Test
public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() {
mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
mZenModeHelper.mConfig.automaticRules.clear();
@@ -4748,18 +4740,17 @@
Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
- mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy,
- UPDATE_ORIGIN_APP);
+ mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy);
ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
.disallowAllSounds()
.allowCalls(PEOPLE_TYPE_CONTACTS)
.allowConversations(CONVERSATION_SENDERS_IMPORTANT)
.hideAllVisualEffects()
- .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+ .allowPriorityChannels(true)
.build();
assertThat(mZenModeHelper.mConfig.automaticRules.values())
- .comparingElementsUsing(IGNORE_TIMESTAMPS)
+ .comparingElementsUsing(IGNORE_METADATA)
.containsExactly(
expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
expectedZenPolicy, /* conditionActive= */ null));
@@ -4774,37 +4765,103 @@
PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
- original, UPDATE_ORIGIN_APP);
+ original);
// Change priorityCallSenders: contacts -> starred.
Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED,
Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
- mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated,
- UPDATE_ORIGIN_APP);
+ mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated);
ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
.disallowAllSounds()
.allowCalls(PEOPLE_TYPE_STARRED)
.allowConversations(CONVERSATION_SENDERS_IMPORTANT)
.hideAllVisualEffects()
- .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
+ .allowPriorityChannels(true)
.build();
assertThat(mZenModeHelper.mConfig.automaticRules.values())
- .comparingElementsUsing(IGNORE_TIMESTAMPS)
+ .comparingElementsUsing(IGNORE_METADATA)
.containsExactly(
expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
expectedZenPolicy, /* conditionActive= */ null));
}
@Test
+ @EnableFlags(android.app.Flags.FLAG_MODES_API)
+ public void applyGlobalPolicyAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() {
+ mZenModeHelper.mConfig.automaticRules.clear();
+ String pkg = mContext.getPackageName();
+
+ // From app, call "setNotificationPolicy" and create and implicit rule.
+ Policy originalPolicy = new Policy(PRIORITY_CATEGORY_MEDIA, 0, 0);
+ mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy);
+ String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet());
+
+ // From user, update that rule's policy.
+ AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ ZenPolicy userUpdateZenPolicy = new ZenPolicy.Builder().disallowAllSounds()
+ .allowAlarms(true).build();
+ AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule)
+ .setZenPolicy(userUpdateZenPolicy)
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason",
+ Process.SYSTEM_UID);
+
+ // From app, call "setNotificationPolicy" again.
+ Policy appUpdatePolicy = new Policy(PRIORITY_CATEGORY_SYSTEM, 0, 0);
+ mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, appUpdatePolicy);
+
+ // The app's update was ignored, and the user's update is still current.
+ assertThat(mZenModeHelper.mConfig.automaticRules.values())
+ .comparingElementsUsing(IGNORE_METADATA)
+ .containsExactly(
+ expectedImplicitRule(pkg, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+ userUpdateZenPolicy,
+ /* conditionActive= */ null));
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_MODES_API)
+ public void applyGlobalPolicyAsImplicitZenRule_ruleCustomizedButNotZenPolicy_updatesRule() {
+ mZenModeHelper.mConfig.automaticRules.clear();
+ String pkg = mContext.getPackageName();
+
+ // From app, call "setNotificationPolicy" and create and implicit rule.
+ Policy originalPolicy = new Policy(PRIORITY_CATEGORY_MEDIA, 0, 0);
+ mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy);
+ String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet());
+
+ // From user, update something in that rule, but not the ZenPolicy.
+ AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule)
+ .setName("Rule renamed, not touching policy")
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason",
+ Process.SYSTEM_UID);
+
+ // From app, call "setNotificationPolicy" again.
+ Policy appUpdatePolicy = new Policy(PRIORITY_CATEGORY_SYSTEM, 0, 0);
+ mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, appUpdatePolicy);
+
+ // The app's update was applied.
+ ZenPolicy appsSecondZenPolicy = new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .allowSystem(true)
+ .allowPriorityChannels(true)
+ .build();
+ assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenPolicy)
+ .isEqualTo(appsSecondZenPolicy);
+ }
+
+ @Test
public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() {
mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API);
mZenModeHelper.mConfig.automaticRules.clear();
withoutWtfCrash(
() -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME,
- CUSTOM_PKG_UID, new Policy(0, 0, 0), UPDATE_ORIGIN_APP));
+ CUSTOM_PKG_UID, new Policy(0, 0, 0)));
assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
}
@@ -4817,7 +4874,7 @@
Policy.getAllSuppressedVisualEffects(), STATE_FALSE,
CONVERSATION_SENDERS_IMPORTANT);
mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
- writtenPolicy, UPDATE_ORIGIN_APP);
+ writtenPolicy);
Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
CUSTOM_PKG_NAME);
@@ -4857,7 +4914,7 @@
assertThat(readPolicy.allowConversations()).isFalse();
}
- private static final Correspondence<ZenRule, ZenRule> IGNORE_TIMESTAMPS =
+ private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA =
Correspondence.transforming(zr -> {
Parcel p = Parcel.obtain();
try {
@@ -4865,12 +4922,15 @@
p.setDataPosition(0);
ZenRule copy = new ZenRule(p);
copy.creationTime = 0;
+ copy.userModifiedFields = 0;
+ copy.zenPolicyUserModifiedFields = 0;
+ copy.zenDeviceEffectsUserModifiedFields = 0;
return copy;
} finally {
p.recycle();
}
},
- "Ignoring timestamps");
+ "Ignoring timestamp and userModifiedFields");
private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy,
@Nullable Boolean conditionActive) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
index 21c96d6..4ed55df 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
@@ -213,13 +213,12 @@
ZenPolicy unset = builder.build();
// priority channels allowed
- builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+ builder.allowPriorityChannels(true);
ZenPolicy channelsPriority = builder.build();
// unset applied, channels setting keeps its state
channelsPriority.apply(unset);
- assertThat(channelsPriority.getAllowedChannels())
- .isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+ assertThat(channelsPriority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
}
@Test
@@ -227,15 +226,15 @@
mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
ZenPolicy.Builder builder = new ZenPolicy.Builder();
- builder.allowChannels(ZenPolicy.CHANNEL_TYPE_NONE);
+ builder.allowPriorityChannels(false);
ZenPolicy none = builder.build();
- builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+ builder.allowPriorityChannels(true);
ZenPolicy priority = builder.build();
// priority channels (less strict state) cannot override a setting that sets it to none
none.apply(priority);
- assertThat(none.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
+ assertThat(none.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
}
@Test
@@ -243,15 +242,15 @@
mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
ZenPolicy.Builder builder = new ZenPolicy.Builder();
- builder.allowChannels(ZenPolicy.CHANNEL_TYPE_NONE);
+ builder.allowPriorityChannels(false);
ZenPolicy none = builder.build();
- builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+ builder.allowPriorityChannels(true);
ZenPolicy priority = builder.build();
// applying a policy with channelType=none overrides priority setting
priority.apply(none);
- assertThat(priority.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
+ assertThat(priority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
}
@Test
@@ -261,12 +260,12 @@
ZenPolicy.Builder builder = new ZenPolicy.Builder();
ZenPolicy unset = builder.build();
- builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+ builder.allowPriorityChannels(true);
ZenPolicy priority = builder.build();
// applying a policy with a set channel type actually goes through
unset.apply(priority);
- assertThat(unset.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+ assertThat(unset.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
}
@Test
@@ -308,7 +307,7 @@
ZenPolicy.Builder builder = new ZenPolicy.Builder();
ZenPolicy policy = builder.build();
- assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET);
+ assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
}
@Test
@@ -622,10 +621,10 @@
// allowChannels should be unset, not be modifiable, and not show up in any output
ZenPolicy.Builder builder = new ZenPolicy.Builder();
- builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+ builder.allowPriorityChannels(true);
ZenPolicy policy = builder.build();
- assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET);
+ assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
assertThat(policy.toString().contains("allowChannels")).isFalse();
}
@@ -635,40 +634,14 @@
// allow priority channels
ZenPolicy.Builder builder = new ZenPolicy.Builder();
- builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+ builder.allowPriorityChannels(true);
ZenPolicy policy = builder.build();
- assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
+ assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
// disallow priority channels
- builder.allowChannels(ZenPolicy.CHANNEL_TYPE_NONE);
+ builder.allowPriorityChannels(false);
policy = builder.build();
- assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE);
- }
-
- @Test
- public void testFromParcel() {
- ZenPolicy.Builder builder = new ZenPolicy.Builder();
- builder.setUserModifiedFields(10);
-
- ZenPolicy policy = builder.build();
- assertThat(policy.getUserModifiedFields()).isEqualTo(10);
-
- Parcel parcel = Parcel.obtain();
- policy.writeToParcel(parcel, 0);
- parcel.setDataPosition(0);
-
- ZenPolicy fromParcel = ZenPolicy.CREATOR.createFromParcel(parcel);
- assertThat(fromParcel.getUserModifiedFields()).isEqualTo(10);
- }
-
- @Test
- public void testPolicy_userModifiedFields() {
- ZenPolicy.Builder builder = new ZenPolicy.Builder();
- builder.setUserModifiedFields(10);
- assertThat(builder.build().getUserModifiedFields()).isEqualTo(10);
-
- builder.setUserModifiedFields(0);
- assertThat(builder.build().getUserModifiedFields()).isEqualTo(0);
+ assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
}
@Test
@@ -676,8 +649,8 @@
ZenPolicy.Builder builder = new ZenPolicy.Builder();
ZenPolicy policy = builder.allowRepeatCallers(true).allowAlarms(false)
.showLights(true).showBadges(false)
- .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY)
- .setUserModifiedFields(20).build();
+ .allowPriorityChannels(true)
+ .build();
ZenPolicy newPolicy = new ZenPolicy.Builder(policy).build();
@@ -689,8 +662,7 @@
assertThat(newPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW);
assertThat(newPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET);
- assertThat(newPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY);
- assertThat(newPolicy.getUserModifiedFields()).isEqualTo(20);
+ assertThat(newPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 9bb2da0..ba7b52e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3365,7 +3365,7 @@
assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
if (Flags.bundleClientTransactionFlag()) {
- verify(app2.getProcess()).scheduleClientTransactionItem(
+ verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem(
isA(WindowStateResizeItem.class));
} else {
verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
diff --git a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
index 2d3c4bb..2ea5dc4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
@@ -157,8 +157,11 @@
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
KEY_SPLASH_SCREEN_EXCEPTION_LIST, commaSeparatedList, false);
try {
- assertTrue("Timed out waiting for DeviceConfig to be updated.",
- latch.await(5, TimeUnit.SECONDS));
+ if (!latch.await(1, TimeUnit.SECONDS)) {
+ Log.w(getClass().getSimpleName(),
+ "Timed out waiting for DeviceConfig to be updated. Force update.");
+ mList.updateDeviceConfig(commaSeparatedList);
+ }
} catch (InterruptedException e) {
Assert.fail(e.getMessage());
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
index 339162a..dfea2fc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
@@ -44,7 +44,6 @@
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
-import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest;
import com.android.server.AnimationThread;
@@ -147,9 +146,10 @@
assertFinishCallbackNotCalled();
}
- @FlakyTest(bugId = 71719744)
@Test
public void testCancel_sneakyCancelBeforeUpdate() throws Exception {
+ final CountDownLatch animationCancelled = new CountDownLatch(1);
+
mSurfaceAnimationRunner = new SurfaceAnimationRunner(null, () -> new ValueAnimator() {
{
setFloatValues(0f, 1f);
@@ -162,6 +162,7 @@
// interleaving of multiple threads. Muahahaha
if (animation.getCurrentPlayTime() > 0) {
mSurfaceAnimationRunner.onAnimationCancelled(mMockSurface);
+ animationCancelled.countDown();
}
listener.onAnimationUpdate(animation);
});
@@ -170,11 +171,7 @@
when(mMockAnimationSpec.getDuration()).thenReturn(200L);
mSurfaceAnimationRunner.startAnimation(mMockAnimationSpec, mMockSurface, mMockTransaction,
this::finishedCallback);
-
- // We need to wait for two frames: The first frame starts the animation, the second frame
- // actually cancels the animation.
- waitUntilNextFrame();
- waitUntilNextFrame();
+ assertTrue(animationCancelled.await(1, SECONDS));
assertTrue(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
verify(mMockAnimationSpec, atLeastOnce()).apply(any(), any(), eq(0L));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index b360800..961fdfb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1822,6 +1822,35 @@
verify(fragment2).assignLayer(t, 2);
}
+ @Test
+ public void testMoveTaskFragmentsToBottomIfNeeded() {
+ final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+ final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+ final ActivityRecord unembeddedActivity = task.getTopMostActivity();
+
+ final TaskFragment fragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer);
+ final TaskFragment fragment2 = createTaskFragmentWithEmbeddedActivity(task, organizer);
+ final TaskFragment fragment3 = createTaskFragmentWithEmbeddedActivity(task, organizer);
+ doReturn(true).when(fragment1).isMoveToBottomIfClearWhenLaunch();
+ doReturn(false).when(fragment2).isMoveToBottomIfClearWhenLaunch();
+ doReturn(true).when(fragment3).isMoveToBottomIfClearWhenLaunch();
+
+ assertEquals(unembeddedActivity, task.mChildren.get(0));
+ assertEquals(fragment1, task.mChildren.get(1));
+ assertEquals(fragment2, task.mChildren.get(2));
+ assertEquals(fragment3, task.mChildren.get(3));
+
+ final int[] finishCount = {0};
+ task.moveTaskFragmentsToBottomIfNeeded(unembeddedActivity, finishCount);
+
+ // fragment1 and fragment3 should be moved to the bottom of the task
+ assertEquals(fragment1, task.mChildren.get(0));
+ assertEquals(fragment3, task.mChildren.get(1));
+ assertEquals(unembeddedActivity, task.mChildren.get(2));
+ assertEquals(fragment2, task.mChildren.get(3));
+ assertEquals(2, finishCount[0]);
+ }
+
private Task getTestTask() {
return new TaskBuilder(mSupervisor).setCreateActivity(true).build();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 9c421ba..7ae5a11 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -1062,6 +1062,8 @@
mWm.mAnimator.ready();
if (!mWm.mWindowPlacerLocked.isTraversalScheduled()) {
mRootWindowContainer.performSurfacePlacement();
+ } else {
+ waitHandlerIdle(mWm.mAnimationHandler);
}
waitUntilWindowAnimatorIdle();
}
diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java
index 24d3918..fe699af 100644
--- a/telecomm/java/android/telecom/CallControl.java
+++ b/telecomm/java/android/telecom/CallControl.java
@@ -19,6 +19,7 @@
import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -32,6 +33,7 @@
import com.android.internal.telecom.ClientTransactionalServiceRepository;
import com.android.internal.telecom.ICallControl;
+import com.android.server.telecom.flags.Flags;
import java.util.List;
import java.util.Objects;
@@ -292,6 +294,43 @@
}
/**
+ * Request a new mute state. Note: {@link CallEventCallback#onMuteStateChanged(boolean)}
+ * will be called every time the mute state is changed and can be used to track the current
+ * mute state.
+ *
+ * @param isMuted The new mute state. Passing in a {@link Boolean#TRUE} for the isMuted
+ * parameter will mute the call. {@link Boolean#FALSE} will unmute the call.
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
+ * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side
+ * that details success or failure of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has
+ * successfully changed the mute state.
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
+ * switch to the mute state. A {@link CallException} will be
+ * passed that details why the operation failed.
+ */
+ @FlaggedApi(Flags.FLAG_SET_MUTE_STATE)
+ public void setMuteState(boolean isMuted, @CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, CallException> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ if (mServerInterface != null) {
+ try {
+ mServerInterface.setMuteState(isMuted,
+ new CallControlResultReceiver("setMuteState", executor, callback));
+
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ } else {
+ throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ }
+ }
+
+ /**
* Raises an event to the {@link android.telecom.InCallService} implementations tracking this
* call via {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)}.
* These events and the associated extra keys for the {@code Bundle} parameter are mutually
diff --git a/telecomm/java/com/android/internal/telecom/ICallControl.aidl b/telecomm/java/com/android/internal/telecom/ICallControl.aidl
index 5e2c923..372e4a12 100644
--- a/telecomm/java/com/android/internal/telecom/ICallControl.aidl
+++ b/telecomm/java/com/android/internal/telecom/ICallControl.aidl
@@ -32,5 +32,6 @@
void disconnect(String callId, in DisconnectCause disconnectCause, in ResultReceiver callback);
void startCallStreaming(String callId, in ResultReceiver callback);
void requestCallEndpointChange(in CallEndpoint callEndpoint, in ResultReceiver callback);
+ void setMuteState(boolean isMuted, in ResultReceiver callback);
void sendEvent(String callId, String event, in Bundle extras);
}
\ No newline at end of file
diff --git a/telephony/java/android/telephony/ims/ImsService.java b/telephony/java/android/telephony/ims/ImsService.java
index 4c37f7d..b84ff29 100644
--- a/telephony/java/android/telephony/ims/ImsService.java
+++ b/telephony/java/android/telephony/ims/ImsService.java
@@ -16,6 +16,7 @@
package android.telephony.ims;
+import android.annotation.FlaggedApi;
import android.annotation.LongDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -46,6 +47,7 @@
import com.android.ims.internal.IImsFeatureStatusCallback;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.flags.Flags;
import com.android.internal.telephony.util.TelephonyUtils;
import java.lang.annotation.Retention;
@@ -152,12 +154,36 @@
public static final long CAPABILITY_TERMINAL_BASED_CALL_WAITING = 1 << 2;
/**
+ * This ImsService supports the capability to manage calls on multiple subscriptions at the same
+ * time.
+ * <p>
+ * When set, this ImsService supports managing calls on multiple subscriptions at the same time
+ * for all WLAN network configurations. Telephony will allow new outgoing/incoming IMS calls to
+ * be set up on other subscriptions while there is an ongoing call. The ImsService must also
+ * support managing calls on WWAN + WWAN configurations whenever the modem also reports
+ * simultaneous calling availability, which can be listened to using the
+ * {@link android.telephony.TelephonyCallback.SimultaneousCellularCallingSupportListener} API.
+ * Telephony will only allow additional ongoing/incoming IMS calls on another subscription to be
+ * set up on WWAN + WWAN configurations when the modem reports that simultaneous cellular
+ * calling is allowed at the current time on both subscriptions where there are ongoing calls.
+ * <p>
+ * When unset (default), this ImsService can not support calls on multiple subscriptions at the
+ * same time for any WLAN or WWAN configurations, so pending outgoing call placed on another
+ * cellular subscription while there is an ongoing call will be cancelled by Telephony.
+ * Similarly, any incoming call notification on another cellular subscription while there is an
+ * ongoing call will be rejected.
+ * @hide TODO: move this to system API when we have a backing implementation + CTS testing
+ */
+ @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS)
+ public static final long CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING = 1 << 3;
+
+ /**
* Used for internal correctness checks of capabilities set by the ImsService implementation and
* tracks the index of the largest defined flag in the capabilities long.
* @hide
*/
public static final long CAPABILITY_MAX_INDEX =
- Long.numberOfTrailingZeros(CAPABILITY_TERMINAL_BASED_CALL_WAITING);
+ Long.numberOfTrailingZeros(CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING);
/**
* @hide
diff --git a/test-mock/src/android/test/mock/MockContext.java b/test-mock/src/android/test/mock/MockContext.java
index f5f9d97..cf38bea 100644
--- a/test-mock/src/android/test/mock/MockContext.java
+++ b/test-mock/src/android/test/mock/MockContext.java
@@ -822,12 +822,6 @@
throw new UnsupportedOperationException();
}
- /** {@hide} */
- @Override
- public int getUserId() {
- throw new UnsupportedOperationException();
- }
-
@Override
public Context createConfigurationContext(Configuration overrideConfiguration) {
throw new UnsupportedOperationException();
diff --git a/tests/Camera2Tests/CameraToo/tests/Android.bp b/tests/Camera2Tests/CameraToo/tests/Android.bp
new file mode 100644
index 0000000..8339a2c
--- /dev/null
+++ b/tests/Camera2Tests/CameraToo/tests/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2014 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
+ default_applicable_licenses: [
+ "frameworks_base_license",
+ ],
+}
+
+android_test {
+ name: "CameraTooTests",
+ instrumentation_for: "CameraToo",
+ srcs: ["src/**/*.java"],
+ sdk_version: "current",
+ static_libs: [
+ "androidx.test.rules",
+ "mockito-target-minus-junit4",
+ ],
+ data: [
+ ":CameraToo",
+ ],
+}
diff --git a/tests/Camera2Tests/CameraToo/tests/Android.mk b/tests/Camera2Tests/CameraToo/tests/Android.mk
deleted file mode 100644
index dfa64f1..0000000
--- a/tests/Camera2Tests/CameraToo/tests/Android.mk
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright (C) 2014 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.
-
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := CameraTooTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../NOTICE
-LOCAL_INSTRUMENTATION_FOR := CameraToo
-LOCAL_SDK_VERSION := current
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules mockito-target-minus-junit4
-
-include $(BUILD_PACKAGE)
diff --git a/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml b/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml
new file mode 100644
index 0000000..884c095
--- /dev/null
+++ b/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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 CameraToo 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="test-file-name" value="CameraTooTests.apk" />
+ <option name="test-file-name" value="CameraToo.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.example.android.camera2.cameratoo.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
index e3988cd..c44d943 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -68,7 +68,7 @@
// This triggers a recalcuation of splitatributes.
ActivityEmbeddingController
.getInstance(ActivityEmbeddingSecondaryActivity.this)
- .invalidateTopVisibleActivityStacks();
+ .invalidateVisibleActivityStacks();
}
});
findViewById(R.id.secondary_enter_pip_button).setOnClickListener(
diff --git a/tests/FsVerityTest/AndroidTest.xml b/tests/FsVerityTest/AndroidTest.xml
index d2537f6..f2d7990 100644
--- a/tests/FsVerityTest/AndroidTest.xml
+++ b/tests/FsVerityTest/AndroidTest.xml
@@ -15,6 +15,7 @@
-->
<configuration description="fs-verity end-to-end test">
<option name="test-suite-tag" value="apct" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user"/>
<object type="module_controller" class="com.android.tradefed.testtype.suite.module.ShippingApiLevelModuleController">
<!-- fs-verity is required since R/30 -->
diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
index bbd4567..7343ba1c 100644
--- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt
@@ -16,6 +16,7 @@
package com.android.server.input
+import android.app.NotificationManager
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ActivityInfo
@@ -129,6 +130,8 @@
@Mock
private lateinit var packageManager: PackageManager
+ @Mock
+ private lateinit var notificationManager: NotificationManager
private lateinit var keyboardLayoutManager: KeyboardLayoutManager
private lateinit var imeInfo: InputMethodInfo
@@ -163,6 +166,8 @@
keyboardLayoutManager = Mockito.spy(
KeyboardLayoutManager(context, native, dataStore, testLooper.looper)
)
+ Mockito.`when`(context.getSystemService(Mockito.eq(Context.NOTIFICATION_SERVICE)))
+ .thenReturn(notificationManager)
setupInputDevices()
setupBroadcastReceiver()
setupIme()
@@ -946,6 +951,48 @@
}
}
+ @Test
+ fun testNotificationShown_onInputDeviceChanged() {
+ val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype()))
+ Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping
+ Mockito.doReturn(false).`when`(keyboardLayoutManager).isVirtualDevice(
+ ArgumentMatchers.eq(keyboardDevice.id)
+ )
+ NewSettingsApiFlag(true).use {
+ keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id)
+ ExtendedMockito.verify(
+ notificationManager,
+ Mockito.times(1)
+ ).notifyAsUser(
+ ArgumentMatchers.isNull(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any()
+ )
+ }
+ }
+
+ @Test
+ fun testNotificationNotShown_onInputDeviceChanged_forVirtualDevice() {
+ val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype()))
+ Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping
+ Mockito.doReturn(true).`when`(keyboardLayoutManager).isVirtualDevice(
+ ArgumentMatchers.eq(keyboardDevice.id)
+ )
+ NewSettingsApiFlag(true).use {
+ keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id)
+ ExtendedMockito.verify(
+ notificationManager,
+ Mockito.never()
+ ).notifyAsUser(
+ ArgumentMatchers.isNull(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any()
+ )
+ }
+ }
+
private fun assertCorrectLayout(
device: InputDevice,
imeSubtype: InputMethodSubtype,
diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk
deleted file mode 100644
index 6361f9b..0000000
--- a/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk
+++ /dev/null
@@ -1,2 +0,0 @@
-LOCAL_PATH := $(call my-dir)
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk
deleted file mode 100644
index 27b6068..0000000
--- a/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk
+++ /dev/null
@@ -1,32 +0,0 @@
-//
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_PACKAGE_NAME := AaptTestMergeOnly_App
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_EXPORT_PACKAGE_RESOURCES := true
-LOCAL_MODULE_TAGS := tests
-LOCAL_STATIC_ANDROID_LIBRARIES := \
- AaptTestMergeOnly_LeafLib \
- AaptTestMergeOnly_LocalLib
-include $(BUILD_PACKAGE)
diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk
deleted file mode 100644
index c084849..0000000
--- a/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_MODULE := AaptTestMergeOnly_LeafLib
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_MIN_SDK_VERSION := 21
-LOCAL_AAPT_FLAGS := --merge-only
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk
deleted file mode 100644
index 699ad79..0000000
--- a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_MODULE := AaptTestMergeOnly_LocalLib
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_MIN_SDK_VERSION := 21
-LOCAL_AAPT_FLAGS := --merge-only
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/Android.mk
deleted file mode 100644
index 6361f9b..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/Android.mk
+++ /dev/null
@@ -1,2 +0,0 @@
-LOCAL_PATH := $(call my-dir)
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk
deleted file mode 100644
index 98b7440..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk
+++ /dev/null
@@ -1,33 +0,0 @@
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_PACKAGE_NAME := AaptTestNamespace_App
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_EXPORT_PACKAGE_RESOURCES := true
-LOCAL_MODULE_TAGS := tests
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_STATIC_ANDROID_LIBRARIES := \
- AaptTestNamespace_LibOne \
- AaptTestNamespace_LibTwo
-include $(BUILD_PACKAGE)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk
deleted file mode 100644
index dd41702..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk
+++ /dev/null
@@ -1,33 +0,0 @@
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_MODULE := AaptTestNamespace_LibOne
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_MIN_SDK_VERSION := 21
-
-# We need this to retain the R.java generated for this library.
-LOCAL_JAR_EXCLUDE_FILES := none
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk
deleted file mode 100644
index 0d11bcb..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk
+++ /dev/null
@@ -1,34 +0,0 @@
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_MODULE := AaptTestNamespace_LibTwo
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_MIN_SDK_VERSION := 21
-
-# We need this to retain the R.java generated for this library.
-LOCAL_JAR_EXCLUDE_FILES := none
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk
deleted file mode 100644
index 30375728..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk
+++ /dev/null
@@ -1,32 +0,0 @@
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_PACKAGE_NAME := AaptTestNamespace_Split
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_APK_LIBRARIES := AaptTestNamespace_App
-LOCAL_RES_LIBRARIES := AaptTestNamespace_App
-LOCAL_AAPT_FLAGS := --package-id 0x80 --rename-manifest-package com.android.aapt.namespace.app
-include $(BUILD_PACKAGE)