Merge "Stylus: Introduce Show Stylus Hover Pointer Setting (1/2)" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 9ee74e3..1c6df75 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -56,6 +56,7 @@
":android.service.notification.flags-aconfig-java{.generated_srcjars}",
":android.service.voice.flags-aconfig-java{.generated_srcjars}",
":android.speech.flags-aconfig-java{.generated_srcjars}",
+ ":android.systemserver.flags-aconfig-java{.generated_srcjars}",
":android.tracing.flags-aconfig-java{.generated_srcjars}",
":android.view.accessibility.flags-aconfig-java{.generated_srcjars}",
":android.view.contentcapture.flags-aconfig-java{.generated_srcjars}",
@@ -1159,3 +1160,16 @@
host_supported: true,
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+
+// System Server
+aconfig_declarations {
+ name: "android.systemserver.flags-aconfig",
+ package: "android.server",
+ srcs: ["services/java/com/android/server/flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "android.systemserver.flags-aconfig-java",
+ aconfig_declarations: "android.systemserver.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index cea16d6..e6ee975 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -318,7 +318,8 @@
private final List<JobRestriction> mJobRestrictions;
@GuardedBy("mLock")
- private final BatteryStateTracker mBatteryStateTracker;
+ @VisibleForTesting
+ final BatteryStateTracker mBatteryStateTracker;
@GuardedBy("mLock")
private final SparseArray<String> mCloudMediaProviderPackages = new SparseArray<>();
@@ -4261,20 +4262,34 @@
.sendToTarget();
}
- private final class BatteryStateTracker extends BroadcastReceiver {
- /**
- * Track whether we're "charging", where charging means that we're ready to commit to
- * doing work.
- */
- private boolean mCharging;
+ @VisibleForTesting
+ final class BatteryStateTracker extends BroadcastReceiver
+ implements BatteryManagerInternal.ChargingPolicyChangeListener {
+ private final BatteryManagerInternal mBatteryManagerInternal;
+
+ /** Last reported battery level. */
+ private int mBatteryLevel;
/** Keep track of whether the battery is charged enough that we want to do work. */
private boolean mBatteryNotLow;
+ /**
+ * Charging status based on {@link BatteryManager#ACTION_CHARGING} and
+ * {@link BatteryManager#ACTION_DISCHARGING}.
+ */
+ private boolean mCharging;
+ /**
+ * The most recently acquired value of
+ * {@link BatteryManager#BATTERY_PROPERTY_CHARGING_POLICY}.
+ */
+ private int mChargingPolicy;
+ /** Track whether there is power connected. It doesn't mean the device is charging. */
+ private boolean mPowerConnected;
/** Sequence number of last broadcast. */
private int mLastBatterySeq = -1;
private BroadcastReceiver mMonitor;
BatteryStateTracker() {
+ mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class);
}
public void startTracking() {
@@ -4286,13 +4301,18 @@
// Charging/not charging.
filter.addAction(BatteryManager.ACTION_CHARGING);
filter.addAction(BatteryManager.ACTION_DISCHARGING);
+ filter.addAction(Intent.ACTION_BATTERY_LEVEL_CHANGED);
+ filter.addAction(Intent.ACTION_POWER_CONNECTED);
+ filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
getTestableContext().registerReceiver(this, filter);
+ mBatteryManagerInternal.registerChargingPolicyChangeListener(this);
+
// Initialise tracker state.
- BatteryManagerInternal batteryManagerInternal =
- LocalServices.getService(BatteryManagerInternal.class);
- mBatteryNotLow = !batteryManagerInternal.getBatteryLevelLow();
- mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
+ mBatteryLevel = mBatteryManagerInternal.getBatteryLevel();
+ mBatteryNotLow = !mBatteryManagerInternal.getBatteryLevelLow();
+ mCharging = mBatteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
+ mChargingPolicy = mBatteryManagerInternal.getChargingPolicy();
}
public void setMonitorBatteryLocked(boolean enabled) {
@@ -4315,7 +4335,7 @@
}
public boolean isCharging() {
- return mCharging;
+ return isConsideredCharging();
}
public boolean isBatteryNotLow() {
@@ -4326,17 +4346,42 @@
return mMonitor != null;
}
+ public boolean isPowerConnected() {
+ return mPowerConnected;
+ }
+
public int getSeq() {
return mLastBatterySeq;
}
@Override
+ public void onChargingPolicyChanged(int newPolicy) {
+ synchronized (mLock) {
+ if (mChargingPolicy == newPolicy) {
+ return;
+ }
+ if (DEBUG) {
+ Slog.i(TAG,
+ "Charging policy changed from " + mChargingPolicy + " to " + newPolicy);
+ }
+
+ final boolean wasConsideredCharging = isConsideredCharging();
+ mChargingPolicy = newPolicy;
+
+ if (isConsideredCharging() != wasConsideredCharging) {
+ for (int c = mControllers.size() - 1; c >= 0; --c) {
+ mControllers.get(c).onBatteryStateChangedLocked();
+ }
+ }
+ }
+ }
+
+ @Override
public void onReceive(Context context, Intent intent) {
onReceiveInternal(intent);
}
- @VisibleForTesting
- public void onReceiveInternal(Intent intent) {
+ private void onReceiveInternal(Intent intent) {
synchronized (mLock) {
final String action = intent.getAction();
boolean changed = false;
@@ -4356,21 +4401,49 @@
mBatteryNotLow = true;
changed = true;
}
+ } else if (Intent.ACTION_BATTERY_LEVEL_CHANGED.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Battery level changed @ "
+ + sElapsedRealtimeClock.millis());
+ }
+ final boolean wasConsideredCharging = isConsideredCharging();
+ mBatteryLevel = mBatteryManagerInternal.getBatteryLevel();
+ changed = isConsideredCharging() != wasConsideredCharging;
+ } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Power connected @ " + sElapsedRealtimeClock.millis());
+ }
+ if (mPowerConnected) {
+ return;
+ }
+ mPowerConnected = true;
+ changed = true;
+ } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Power disconnected @ " + sElapsedRealtimeClock.millis());
+ }
+ if (!mPowerConnected) {
+ return;
+ }
+ mPowerConnected = false;
+ changed = true;
} else if (BatteryManager.ACTION_CHARGING.equals(action)) {
if (DEBUG) {
Slog.d(TAG, "Battery charging @ " + sElapsedRealtimeClock.millis());
}
if (!mCharging) {
+ final boolean wasConsideredCharging = isConsideredCharging();
mCharging = true;
- changed = true;
+ changed = isConsideredCharging() != wasConsideredCharging;
}
} else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
if (DEBUG) {
Slog.d(TAG, "Battery discharging @ " + sElapsedRealtimeClock.millis());
}
if (mCharging) {
+ final boolean wasConsideredCharging = isConsideredCharging();
mCharging = false;
- changed = true;
+ changed = isConsideredCharging() != wasConsideredCharging;
}
}
mLastBatterySeq =
@@ -4382,6 +4455,30 @@
}
}
}
+
+ private boolean isConsideredCharging() {
+ if (mCharging) {
+ return true;
+ }
+ // BatteryService (or Health HAL or whatever central location makes sense)
+ // should ideally hold this logic so that everyone has a consistent
+ // idea of when the device is charging (or an otherwise stable charging/plugged state).
+ // TODO(304512874): move this determination to BatteryService
+ if (!mPowerConnected) {
+ return false;
+ }
+
+ if (mChargingPolicy == Integer.MIN_VALUE) {
+ // Property not supported on this device.
+ return false;
+ }
+ // Adaptive charging policies don't expose their target battery level, but 80% is a
+ // commonly used threshold for battery health, so assume that's what's being used by
+ // the policies and use 70%+ as the threshold here for charging in case some
+ // implementations choose to discharge the device slightly before recharging back up
+ // to the target level.
+ return mBatteryLevel >= 70 && BatteryManager.isAdaptiveChargingPolicy(mChargingPolicy);
+ }
}
final class LocalService implements JobSchedulerInternal {
@@ -5450,6 +5547,13 @@
}
}
+ /** Return {@code true} if the device is connected to power. */
+ public boolean isPowerConnected() {
+ synchronized (mLock) {
+ return mBatteryStateTracker.isPowerConnected();
+ }
+ }
+
int getStorageSeq() {
synchronized (mLock) {
return mStorageController.getTracker().getSeq();
@@ -5778,8 +5882,14 @@
mQuotaTracker.dump(pw);
pw.println();
+ pw.print("Power connected: ");
+ pw.println(mBatteryStateTracker.isPowerConnected());
pw.print("Battery charging: ");
- pw.println(mBatteryStateTracker.isCharging());
+ pw.println(mBatteryStateTracker.mCharging);
+ pw.print("Considered charging: ");
+ pw.println(mBatteryStateTracker.isConsideredCharging());
+ pw.print("Battery level: ");
+ pw.println(mBatteryStateTracker.mBatteryLevel);
pw.print("Battery not low: ");
pw.println(mBatteryStateTracker.isBatteryNotLow());
if (mBatteryStateTracker.isMonitoring()) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
index ddbc2ec..e9f9b14 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
@@ -20,12 +20,6 @@
import android.annotation.NonNull;
import android.app.job.JobInfo;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
-import android.os.BatteryManagerInternal;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
@@ -36,7 +30,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.AppSchedulingModuleThread;
-import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateControllerProto;
@@ -60,8 +53,6 @@
@GuardedBy("mLock")
private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>();
- private final PowerTracker mPowerTracker;
-
private final FlexibilityController mFlexibilityController;
/**
* Helper set to avoid too much GC churn from frequent calls to
@@ -77,16 +68,10 @@
public BatteryController(JobSchedulerService service,
FlexibilityController flexibilityController) {
super(service);
- mPowerTracker = new PowerTracker();
mFlexibilityController = flexibilityController;
}
@Override
- public void startTrackingLocked() {
- mPowerTracker.startTracking();
- }
-
- @Override
public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
if (taskStatus.hasPowerConstraint()) {
final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -95,7 +80,7 @@
if (taskStatus.hasChargingConstraint()) {
if (hasTopExemptionLocked(taskStatus)) {
taskStatus.setChargingConstraintSatisfied(nowElapsed,
- mPowerTracker.isPowerConnected());
+ mService.isPowerConnected());
} else {
taskStatus.setChargingConstraintSatisfied(nowElapsed,
mService.isBatteryCharging() && mService.isBatteryNotLow());
@@ -178,7 +163,7 @@
@GuardedBy("mLock")
private void maybeReportNewChargingStateLocked() {
- final boolean powerConnected = mPowerTracker.isPowerConnected();
+ final boolean powerConnected = mService.isPowerConnected();
final boolean stablePower = mService.isBatteryCharging() && mService.isBatteryNotLow();
final boolean batteryNotLow = mService.isBatteryNotLow();
if (DEBUG) {
@@ -239,62 +224,6 @@
mChangedJobs.clear();
}
- private final class PowerTracker extends BroadcastReceiver {
- /**
- * Track whether there is power connected. It doesn't mean the device is charging.
- * Use {@link JobSchedulerService#isBatteryCharging()} to determine if the device is
- * charging.
- */
- private boolean mPowerConnected;
-
- PowerTracker() {
- }
-
- void startTracking() {
- IntentFilter filter = new IntentFilter();
-
- filter.addAction(Intent.ACTION_POWER_CONNECTED);
- filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
- mContext.registerReceiver(this, filter);
-
- // Initialize tracker state.
- BatteryManagerInternal batteryManagerInternal =
- LocalServices.getService(BatteryManagerInternal.class);
- mPowerConnected = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
- }
-
- boolean isPowerConnected() {
- return mPowerConnected;
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- synchronized (mLock) {
- final String action = intent.getAction();
-
- if (Intent.ACTION_POWER_CONNECTED.equals(action)) {
- if (DEBUG) {
- Slog.d(TAG, "Power connected @ " + sElapsedRealtimeClock.millis());
- }
- if (mPowerConnected) {
- return;
- }
- mPowerConnected = true;
- } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
- if (DEBUG) {
- Slog.d(TAG, "Power disconnected @ " + sElapsedRealtimeClock.millis());
- }
- if (!mPowerConnected) {
- return;
- }
- mPowerConnected = false;
- }
-
- maybeReportNewChargingStateLocked();
- }
- }
- }
-
@VisibleForTesting
ArraySet<JobStatus> getTrackedJobs() {
return mTrackedTasks;
@@ -308,7 +237,6 @@
@Override
public void dumpControllerStateLocked(IndentingPrintWriter pw,
Predicate<JobStatus> predicate) {
- pw.println("Power connected: " + mPowerTracker.isPowerConnected());
pw.println("Stable power: " + (mService.isBatteryCharging() && mService.isBatteryNotLow()));
pw.println("Not low: " + mService.isBatteryNotLow());
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 2ea980d..a3a686f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -2053,6 +2053,11 @@
case CONSTRAINT_WITHIN_QUOTA:
return JobParameters.STOP_REASON_QUOTA;
+ // This can change from true to false, but should never change when a job is already
+ // running, so there's no reason to log a message or create a new stop reason.
+ case CONSTRAINT_FLEXIBLE:
+ return JobParameters.STOP_REASON_UNDEFINED;
+
// These should never be stop reasons since they can never go from true to false.
case CONSTRAINT_CONTENT_TRIGGER:
case CONSTRAINT_DEADLINE:
diff --git a/core/api/current.txt b/core/api/current.txt
index 2e3fe5b..bca15bd 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -765,7 +765,7 @@
field public static final int endY = 16844051; // 0x1010513
field @Deprecated public static final int endYear = 16843133; // 0x101017d
field public static final int enforceNavigationBarContrast = 16844293; // 0x1010605
- field public static final int enforceStatusBarContrast = 16844292; // 0x1010604
+ field @Deprecated public static final int enforceStatusBarContrast = 16844292; // 0x1010604
field public static final int enterFadeDuration = 16843532; // 0x101030c
field public static final int entries = 16842930; // 0x10100b2
field public static final int entryValues = 16843256; // 0x10101f8
@@ -1196,8 +1196,8 @@
field public static final int multiprocess = 16842771; // 0x1010013
field public static final int name = 16842755; // 0x1010003
field public static final int nativeHeapZeroInitialized = 16844325; // 0x1010625
- field public static final int navigationBarColor = 16843858; // 0x1010452
- field public static final int navigationBarDividerColor = 16844141; // 0x101056d
+ field @Deprecated public static final int navigationBarColor = 16843858; // 0x1010452
+ field @Deprecated public static final int navigationBarDividerColor = 16844141; // 0x101056d
field public static final int navigationContentDescription = 16843969; // 0x10104c1
field public static final int navigationIcon = 16843968; // 0x10104c0
field public static final int navigationMode = 16843471; // 0x10102cf
@@ -1568,7 +1568,7 @@
field public static final int state_single = 16842915; // 0x10100a3
field public static final int state_window_focused = 16842909; // 0x101009d
field public static final int staticWallpaperPreview = 16843569; // 0x1010331
- field public static final int statusBarColor = 16843857; // 0x1010451
+ field @Deprecated public static final int statusBarColor = 16843857; // 0x1010451
field public static final int stepSize = 16843078; // 0x1010146
field public static final int stopWithTask = 16843626; // 0x101036a
field public static final int streamType = 16843273; // 0x1010209
@@ -1890,6 +1890,7 @@
field public static final int windowNoDisplay = 16843294; // 0x101021e
field public static final int windowNoMoveAnimation = 16844421; // 0x1010685
field public static final int windowNoTitle = 16842838; // 0x1010056
+ field @FlaggedApi("com.android.window.flags.enforce_edge_to_edge") public static final int windowOptOutEdgeToEdgeEnforcement;
field @Deprecated public static final int windowOverscan = 16843727; // 0x10103cf
field public static final int windowReenterTransition = 16843951; // 0x10104af
field public static final int windowReturnTransition = 16843950; // 0x10104ae
@@ -7894,6 +7895,7 @@
field public static final String AUTO_TIME_POLICY = "autoTime";
field public static final String BACKUP_SERVICE_POLICY = "backupService";
field public static final String CAMERA_DISABLED_POLICY = "cameraDisabled";
+ field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final String CONTENT_PROTECTION_POLICY = "contentProtection";
field public static final String KEYGUARD_DISABLED_FEATURES_POLICY = "keyguardDisabledFeatures";
field public static final String LOCK_TASK_POLICY = "lockTask";
field public static final String PACKAGES_SUSPENDED_POLICY = "packagesSuspended";
@@ -7944,6 +7946,7 @@
method public boolean getBluetoothContactSharingDisabled(@NonNull android.content.ComponentName);
method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_CAMERA, conditional=true) public boolean getCameraDisabled(@Nullable android.content.ComponentName);
method @Deprecated @Nullable public String getCertInstallerPackage(@NonNull android.content.ComponentName) throws java.lang.SecurityException;
+ method @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional=true) public int getContentProtectionPolicy(@Nullable android.content.ComponentName);
method @Nullable public android.app.admin.PackagePolicy getCredentialManagerPolicy();
method @Deprecated @Nullable public java.util.Set<java.lang.String> getCrossProfileCalendarPackages(@NonNull android.content.ComponentName);
method @Deprecated public boolean getCrossProfileCallerIdDisabled(@NonNull android.content.ComponentName);
@@ -8100,6 +8103,7 @@
method @Deprecated public void setCertInstallerPackage(@NonNull android.content.ComponentName, @Nullable String) throws java.lang.SecurityException;
method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE, conditional=true) public void setCommonCriteriaModeEnabled(@Nullable android.content.ComponentName, boolean);
method @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_WIFI, conditional=true) public void setConfiguredNetworksLockdownState(@Nullable android.content.ComponentName, boolean);
+ method @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") @RequiresPermission(value=android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional=true) public void setContentProtectionPolicy(@Nullable android.content.ComponentName, int);
method public void setCredentialManagerPolicy(@Nullable android.app.admin.PackagePolicy);
method @Deprecated public void setCrossProfileCalendarPackages(@NonNull android.content.ComponentName, @Nullable java.util.Set<java.lang.String>);
method @Deprecated public void setCrossProfileCallerIdDisabled(@NonNull android.content.ComponentName, boolean);
@@ -8525,6 +8529,7 @@
field public static final int TAG_ADB_SHELL_CMD = 210002; // 0x33452
field public static final int TAG_ADB_SHELL_INTERACTIVE = 210001; // 0x33451
field public static final int TAG_APP_PROCESS_START = 210005; // 0x33455
+ field @FlaggedApi("android.app.admin.flags.backup_service_security_log_event_enabled") public static final int TAG_BACKUP_SERVICE_TOGGLED = 210044; // 0x3347c
field public static final int TAG_BLUETOOTH_CONNECTION = 210039; // 0x33477
field public static final int TAG_BLUETOOTH_DISCONNECTION = 210040; // 0x33478
field public static final int TAG_CAMERA_POLICY_SET = 210034; // 0x33472
@@ -16525,8 +16530,8 @@
field public static final int START_HYPHEN_EDIT_NO_EDIT = 0; // 0x0
field public static final int STRIKE_THRU_TEXT_FLAG = 16; // 0x10
field public static final int SUBPIXEL_TEXT_FLAG = 128; // 0x80
- field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int TEXT_RUN_FLAG_LEFT_EDGE = 8192; // 0x2000
- field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 16384; // 0x4000
+ field @FlaggedApi("com.android.text.flags.letter_spacing_justification") public static final int TEXT_RUN_FLAG_LEFT_EDGE = 8192; // 0x2000
+ field @FlaggedApi("com.android.text.flags.letter_spacing_justification") public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 16384; // 0x4000
field public static final int UNDERLINE_TEXT_FLAG = 8; // 0x8
}
@@ -18003,7 +18008,7 @@
field public static final int HYPHENATION_FREQUENCY_FULL = 2; // 0x2
field public static final int HYPHENATION_FREQUENCY_NONE = 0; // 0x0
field public static final int HYPHENATION_FREQUENCY_NORMAL = 1; // 0x1
- field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
+ field @FlaggedApi("com.android.text.flags.letter_spacing_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1
field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0
}
@@ -24706,6 +24711,7 @@
}
public class Ringtone {
+ method protected void finalize();
method public android.media.AudioAttributes getAudioAttributes();
method @Deprecated public int getStreamType();
method public String getTitle(android.content.Context);
@@ -26632,6 +26638,8 @@
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.tv.BroadcastInfoRequest> CREATOR;
field public static final int REQUEST_OPTION_AUTO_UPDATE = 1; // 0x1
+ field @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public static final int REQUEST_OPTION_ONESHOT = 3; // 0x3
+ field @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public static final int REQUEST_OPTION_ONEWAY = 2; // 0x2
field public static final int REQUEST_OPTION_REPEAT = 0; // 0x0
}
@@ -27438,6 +27446,7 @@
method public void notifyTuned(@NonNull android.net.Uri);
method public void notifyTvMessage(int, @NonNull android.os.Bundle);
method public void notifyVideoAvailable();
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void notifyVideoFreezeUpdated(boolean);
method public void notifyVideoUnavailable(int);
method public void onAdBufferReady(@NonNull android.media.tv.AdBuffer);
method public void onAppPrivateCommand(@NonNull String, android.os.Bundle);
@@ -27608,6 +27617,7 @@
method public void setStreamVolume(@FloatRange(from=0.0, to=1.0) float);
method public void setTimeShiftPositionCallback(@Nullable android.media.tv.TvView.TimeShiftPositionCallback);
method public void setTvMessageEnabled(int, boolean);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void setVideoFrozen(boolean);
method public void setZOrderMediaOverlay(boolean);
method public void setZOrderOnTop(boolean);
method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void stopPlayback(int);
@@ -27651,6 +27661,7 @@
method public void onTuned(@NonNull String, @NonNull android.net.Uri);
method public void onTvMessage(@NonNull String, int, @NonNull android.os.Bundle);
method public void onVideoAvailable(String);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onVideoFreezeUpdated(@NonNull String, boolean);
method public void onVideoSizeChanged(String, int, int);
method public void onVideoUnavailable(String, int);
}
@@ -27662,7 +27673,18 @@
@FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public class TvAdManager {
method @NonNull public java.util.List<android.media.tv.ad.TvAdServiceInfo> getTvAdServiceList();
method public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdManager.TvAdServiceCallback);
+ method public void sendAppLinkCommand(@NonNull String, @NonNull android.os.Bundle);
method public void unregisterCallback(@NonNull android.media.tv.ad.TvAdManager.TvAdServiceCallback);
+ field public static final String ACTION_APP_LINK_COMMAND = "android.media.tv.ad.action.APP_LINK_COMMAND";
+ field public static final String APP_LINK_KEY_BACK_URI = "back_uri";
+ field public static final String APP_LINK_KEY_CLASS_NAME = "class_name";
+ field public static final String APP_LINK_KEY_COMMAND_TYPE = "command_type";
+ field public static final String APP_LINK_KEY_PACKAGE_NAME = "package_name";
+ field public static final String APP_LINK_KEY_SERVICE_ID = "service_id";
+ field public static final String INTENT_KEY_AD_SERVICE_ID = "ad_service_id";
+ field public static final String INTENT_KEY_CHANNEL_URI = "channel_uri";
+ field public static final String INTENT_KEY_COMMAND_TYPE = "command_type";
+ field public static final String INTENT_KEY_TV_INPUT_ID = "tv_input_id";
field public static final String SESSION_DATA_KEY_AD_BUFFER = "ad_buffer";
field public static final String SESSION_DATA_KEY_AD_REQUEST = "ad_request";
field public static final String SESSION_DATA_KEY_BROADCAST_INFO_REQUEST = "broadcast_info_request";
@@ -27682,6 +27704,7 @@
@FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public abstract class TvAdService extends android.app.Service {
ctor public TvAdService();
+ method public void onAppLinkCommand(@NonNull android.os.Bundle);
method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent);
method @Nullable public abstract android.media.tv.ad.TvAdService.Session onCreateSession(@NonNull String, @NonNull String);
field public static final String SERVICE_INTERFACE = "android.media.tv.ad.TvAdService";
@@ -27858,6 +27881,7 @@
method public void onAdResponse(@NonNull android.media.tv.AdResponse);
method public void onAvailableSpeeds(@NonNull float[]);
method public void onBroadcastInfoResponse(@NonNull android.media.tv.BroadcastInfoResponse);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onCertificate(@NonNull String, int, @NonNull android.net.http.SslCertificate);
method public void onContentAllowed();
method public void onContentBlocked(@NonNull android.media.tv.TvContentRating);
method public void onCreateBiInteractiveAppRequest(@NonNull android.net.Uri, @Nullable android.os.Bundle);
@@ -27907,11 +27931,13 @@
method public void onTvRecordingInfo(@Nullable android.media.tv.TvRecordingInfo);
method public void onTvRecordingInfoList(@NonNull java.util.List<android.media.tv.TvRecordingInfo>);
method public void onVideoAvailable();
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onVideoFreezeUpdated(boolean);
method public void onVideoUnavailable(int);
method @CallSuper public void removeBroadcastInfo(int);
method @CallSuper public void requestAd(@NonNull android.media.tv.AdRequest);
method @CallSuper public void requestAvailableSpeeds();
method @CallSuper public void requestBroadcastInfo(@NonNull android.media.tv.BroadcastInfoRequest);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") @CallSuper public void requestCertificate(@NonNull String, int);
method @CallSuper public void requestCurrentChannelLcn();
method @CallSuper public void requestCurrentChannelUri();
method @CallSuper public void requestCurrentTvInputId();
@@ -27920,6 +27946,7 @@
method @CallSuper public void requestScheduleRecording(@NonNull String, @NonNull String, @NonNull android.net.Uri, long, long, int, @NonNull android.os.Bundle);
method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") @CallSuper public void requestSelectedTrackInfo();
method @CallSuper public void requestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") @CallSuper public void requestSigning(@NonNull String, @NonNull String, @NonNull String, int, @NonNull byte[]);
method @CallSuper public void requestStartRecording(@NonNull String, @Nullable android.net.Uri);
method @CallSuper public void requestStopRecording(@NonNull String);
method @CallSuper public void requestStreamVolume();
@@ -27969,6 +27996,7 @@
method public void notifyTimeShiftStartPositionChanged(@NonNull String, long);
method public void notifyTimeShiftStatusChanged(@NonNull String, int);
method public void notifyTvMessage(@NonNull int, @NonNull android.os.Bundle);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void notifyVideoFreezeUpdated(boolean);
method public void onAttachedToWindow();
method public void onDetachedFromWindow();
method public void onLayout(boolean, int, int, int, int);
@@ -27979,6 +28007,7 @@
method public void reset();
method public void resetInteractiveApp();
method public void sendAvailableSpeeds(@NonNull float[]);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void sendCertificate(@NonNull String, int, @NonNull android.net.http.SslCertificate);
method public void sendCurrentChannelLcn(int);
method public void sendCurrentChannelUri(@Nullable android.net.Uri);
method public void sendCurrentTvInputId(@Nullable String);
@@ -28013,6 +28042,7 @@
method public void onBiInteractiveAppCreated(@NonNull String, @NonNull android.net.Uri, @Nullable String);
method public void onPlaybackCommandRequest(@NonNull String, @NonNull String, @NonNull android.os.Bundle);
method public void onRequestAvailableSpeeds(@NonNull String);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onRequestCertificate(@NonNull String, @NonNull String, int);
method public void onRequestCurrentChannelLcn(@NonNull String);
method public void onRequestCurrentChannelUri(@NonNull String);
method public void onRequestCurrentTvInputId(@NonNull String);
@@ -28021,6 +28051,7 @@
method public void onRequestScheduleRecording(@NonNull String, @NonNull String, @NonNull String, @NonNull android.net.Uri, long, long, int, @NonNull android.os.Bundle);
method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onRequestSelectedTrackInfo(@NonNull String);
method public void onRequestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
+ method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void onRequestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int, @NonNull byte[]);
method public void onRequestStartRecording(@NonNull String, @NonNull String, @Nullable android.net.Uri);
method public void onRequestStopRecording(@NonNull String, @NonNull String);
method public void onRequestStreamVolume(@NonNull String);
@@ -46273,6 +46304,7 @@
method @NonNull public android.telephony.euicc.EuiccManager createForCardId(int);
method @RequiresPermission("android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS") public void deleteSubscription(int, android.app.PendingIntent);
method @RequiresPermission("android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS") public void downloadSubscription(android.telephony.euicc.DownloadableSubscription, boolean, android.app.PendingIntent);
+ method @FlaggedApi("com.android.internal.telephony.flags.esim_available_memory") @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", "carrier privileges"}) public long getAvailableMemoryInBytes();
method @Nullable public String getEid();
method @Nullable public android.telephony.euicc.EuiccInfo getEuiccInfo();
method public boolean isEnabled();
@@ -46305,6 +46337,7 @@
field public static final int ERROR_SIM_MISSING = 10008; // 0x2718
field public static final int ERROR_TIME_OUT = 10005; // 0x2715
field public static final int ERROR_UNSUPPORTED_VERSION = 10007; // 0x2717
+ field @FlaggedApi("com.android.internal.telephony.flags.esim_available_memory") public static final long EUICC_MEMORY_FIELD_UNAVAILABLE = -1L; // 0xffffffffffffffffL
field public static final String EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE = "android.telephony.euicc.extra.EMBEDDED_SUBSCRIPTION_DETAILED_CODE";
field public static final String EXTRA_EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTION = "android.telephony.euicc.extra.EMBEDDED_SUBSCRIPTION_DOWNLOADABLE_SUBSCRIPTION";
field public static final String EXTRA_EMBEDDED_SUBSCRIPTION_ERROR_CODE = "android.telephony.euicc.extra.EMBEDDED_SUBSCRIPTION_ERROR_CODE";
@@ -47278,7 +47311,7 @@
method public int getLineForOffset(int);
method public int getLineForVertical(int);
method public float getLineLeft(int);
- method @FlaggedApi("com.android.text.flags.inter_character_justification") @IntRange(from=0) public int getLineLetterSpacingUnitCount(@IntRange(from=0) int, boolean);
+ method @FlaggedApi("com.android.text.flags.letter_spacing_justification") @IntRange(from=0) public int getLineLetterSpacingUnitCount(@IntRange(from=0) int, boolean);
method public float getLineMax(int);
method public float getLineRight(int);
method @FlaggedApi("com.android.text.flags.use_bounds_for_width") public final float getLineSpacingAmount();
@@ -47329,7 +47362,7 @@
field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_ANY_OVERLAP;
field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_ALL;
field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_CENTER;
- field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
+ field @FlaggedApi("com.android.text.flags.letter_spacing_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2
field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1
field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0
}
@@ -53593,8 +53626,8 @@
method @NonNull public abstract android.view.LayoutInflater getLayoutInflater();
method protected final int getLocalFeatures();
method public android.media.session.MediaController getMediaController();
- method @ColorInt public abstract int getNavigationBarColor();
- method @ColorInt public int getNavigationBarDividerColor();
+ method @Deprecated @ColorInt public abstract int getNavigationBarColor();
+ method @Deprecated @ColorInt public int getNavigationBarDividerColor();
method @NonNull public android.window.OnBackInvokedDispatcher getOnBackInvokedDispatcher();
method public android.transition.Transition getReenterTransition();
method public android.transition.Transition getReturnTransition();
@@ -53604,7 +53637,7 @@
method public android.transition.Transition getSharedElementReenterTransition();
method public android.transition.Transition getSharedElementReturnTransition();
method public boolean getSharedElementsUseOverlay();
- method @ColorInt public abstract int getStatusBarColor();
+ method @Deprecated @ColorInt public abstract int getStatusBarColor();
method @NonNull public java.util.List<android.graphics.Rect> getSystemGestureExclusionRects();
method public long getTransitionBackgroundFadeDuration();
method public android.transition.TransitionManager getTransitionManager();
@@ -53620,7 +53653,7 @@
method public abstract boolean isFloating();
method public boolean isNavigationBarContrastEnforced();
method public abstract boolean isShortcutKey(int, android.view.KeyEvent);
- method public boolean isStatusBarContrastEnforced();
+ method @Deprecated public boolean isStatusBarContrastEnforced();
method public boolean isWideColorGamut();
method public final void makeActive();
method protected abstract void onActive();
@@ -53652,7 +53685,7 @@
method public abstract void setContentView(android.view.View);
method public abstract void setContentView(android.view.View, android.view.ViewGroup.LayoutParams);
method public abstract void setDecorCaptionShade(int);
- method public void setDecorFitsSystemWindows(boolean);
+ method @Deprecated public void setDecorFitsSystemWindows(boolean);
method protected void setDefaultWindowFormat(int);
method @FlaggedApi("com.android.graphics.hwui.flags.limited_hdr") public void setDesiredHdrHeadroom(@FloatRange(from=0.0f, to=10000.0) float);
method public void setDimAmount(float);
@@ -53674,9 +53707,9 @@
method public void setLocalFocus(boolean, boolean);
method public void setLogo(@DrawableRes int);
method public void setMediaController(android.media.session.MediaController);
- method public abstract void setNavigationBarColor(@ColorInt int);
+ method @Deprecated public abstract void setNavigationBarColor(@ColorInt int);
method public void setNavigationBarContrastEnforced(boolean);
- method public void setNavigationBarDividerColor(@ColorInt int);
+ method @Deprecated public void setNavigationBarDividerColor(@ColorInt int);
method public void setPreferMinimalPostProcessing(boolean);
method public void setReenterTransition(android.transition.Transition);
method public abstract void setResizingCaptionDrawable(android.graphics.drawable.Drawable);
@@ -53688,8 +53721,8 @@
method public void setSharedElementReturnTransition(android.transition.Transition);
method public void setSharedElementsUseOverlay(boolean);
method public void setSoftInputMode(int);
- method public abstract void setStatusBarColor(@ColorInt int);
- method public void setStatusBarContrastEnforced(boolean);
+ method @Deprecated public abstract void setStatusBarColor(@ColorInt int);
+ method @Deprecated public void setStatusBarContrastEnforced(boolean);
method public void setSustainedPerformanceMode(boolean);
method public void setSystemGestureExclusionRects(@NonNull java.util.List<android.graphics.Rect>);
method public abstract void setTitle(CharSequence);
diff --git a/core/api/lint-baseline.txt b/core/api/lint-baseline.txt
index 162f54c..e901f00 100644
--- a/core/api/lint-baseline.txt
+++ b/core/api/lint-baseline.txt
@@ -389,6 +389,12 @@
Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+KotlinOperator: android.graphics.Matrix44#get(int, int):
+ Method can be invoked with an indexing operator from Kotlin: `get` (this is usually desirable; just make sure it makes sense for this type of object)
+KotlinOperator: android.graphics.Matrix44#set(int, int, float):
+ Method can be invoked with an indexing operator from Kotlin: `set` (this is usually desirable; just make sure it makes sense for this type of object)
+
+
RequiresPermission: android.accounts.AccountManager#getAccountsByTypeAndFeatures(String, String[], android.accounts.AccountManagerCallback<android.accounts.Account[]>, android.os.Handler):
Method 'getAccountsByTypeAndFeatures' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.accounts.AccountManager#hasFeatures(android.accounts.Account, String[], android.accounts.AccountManagerCallback<java.lang.Boolean>, android.os.Handler):
@@ -477,6 +483,8 @@
Method 'clearResetPasswordToken' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#generateKeyPair(android.content.ComponentName, String, android.security.keystore.KeyGenParameterSpec, int):
Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName):
+ Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName):
Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName):
@@ -513,6 +521,8 @@
Method 'removeCrossProfileWidgetProvider' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setAlwaysOnVpnPackage(android.content.ComponentName, String, boolean):
Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int):
+ Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskFeatures(android.content.ComponentName, int):
Method 'setLockTaskFeatures' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskPackages(android.content.ComponentName, String[]):
diff --git a/core/api/module-lib-lint-baseline.txt b/core/api/module-lib-lint-baseline.txt
index a2179bc..42c4efc 100644
--- a/core/api/module-lib-lint-baseline.txt
+++ b/core/api/module-lib-lint-baseline.txt
@@ -611,6 +611,8 @@
Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getApplicationExemptions(String):
Method 'getApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName):
+ Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName):
Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName):
@@ -657,6 +659,8 @@
Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setApplicationExemptions(String, java.util.Set<java.lang.Integer>):
Method 'setApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int):
+ Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceProvisioningConfigApplied():
Method 'setDeviceProvisioningConfigApplied' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskFeatures(android.content.ComponentName, int):
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index f36d560..d6a0185 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11,6 +11,7 @@
field public static final String ACCESS_DRM_CERTIFICATES = "android.permission.ACCESS_DRM_CERTIFICATES";
field @Deprecated public static final String ACCESS_FM_RADIO = "android.permission.ACCESS_FM_RADIO";
field public static final String ACCESS_FPS_COUNTER = "android.permission.ACCESS_FPS_COUNTER";
+ field @FlaggedApi("android.multiuser.flags.enable_permission_to_access_hidden_profiles") public static final String ACCESS_HIDDEN_PROFILES_FULL = "android.permission.ACCESS_HIDDEN_PROFILES_FULL";
field public static final String ACCESS_INSTANT_APPS = "android.permission.ACCESS_INSTANT_APPS";
field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ACCESS_LAST_KNOWN_CELL_ID = "android.permission.ACCESS_LAST_KNOWN_CELL_ID";
field public static final String ACCESS_LOCUS_ID_USAGE_STATS = "android.permission.ACCESS_LOCUS_ID_USAGE_STATS";
@@ -3153,7 +3154,9 @@
public class WearableSensingManager {
method @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideData(@NonNull android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideDataStream(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideWearableConnection(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
field public static final int STATUS_ACCESS_DENIED = 5; // 0x5
+ field @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") public static final int STATUS_CHANNEL_ERROR = 7; // 0x7
field public static final int STATUS_SERVICE_UNAVAILABLE = 3; // 0x3
field public static final int STATUS_SUCCESS = 1; // 0x1
field public static final int STATUS_UNKNOWN = 0; // 0x0
@@ -4249,7 +4252,6 @@
public final class UserProperties implements android.os.Parcelable {
method public int describeContents();
method public int getCrossProfileContentSharingStrategy();
- method @FlaggedApi("android.multiuser.support_hiding_profiles") @NonNull public int getProfileApiVisibility();
method public int getShowInQuietMode();
method public int getShowInSharingSurfaces();
method public boolean isCredentialShareableWithParent();
@@ -4259,9 +4261,6 @@
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 @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_HIDDEN = 1; // 0x1
- field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_UNKNOWN = -1; // 0xffffffff
- field @FlaggedApi("android.multiuser.support_hiding_profiles") public static final int PROFILE_API_VISIBILITY_VISIBLE = 0; // 0x0
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
@@ -12404,6 +12403,7 @@
method @Deprecated public int onDownloadSubscription(int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean, boolean);
method @Deprecated public abstract int onEraseSubscriptions(int);
method public int onEraseSubscriptions(int, int);
+ method @FlaggedApi("com.android.internal.telephony.flags.esim_available_memory") public long onGetAvailableMemoryInBytes(int);
method public abstract android.service.euicc.GetDefaultDownloadableSubscriptionListResult onGetDefaultDownloadableSubscriptionList(int, boolean);
method public abstract android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, android.telephony.euicc.DownloadableSubscription, boolean);
method @NonNull public android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean);
@@ -13433,6 +13433,7 @@
method @BinderThread public abstract void onDataProvided(@NonNull android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @BinderThread public abstract void onDataStreamProvided(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @BinderThread public abstract void onQueryServiceStatus(@NonNull java.util.Set<java.lang.Integer>, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>);
+ method @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") @BinderThread public void onSecureWearableConnectionProvided(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @BinderThread public abstract void onStartDetection(@NonNull android.app.ambientcontext.AmbientContextEventRequest, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionResult>);
method public abstract void onStopDetection(@NonNull String);
field public static final String SERVICE_INTERFACE = "android.service.wearable.WearableSensingService";
@@ -14757,6 +14758,7 @@
method public boolean areUiccApplicationsEnabled();
method @Nullable public java.util.List<android.telephony.UiccAccessRule> getAccessRules();
method public int getProfileClass();
+ method @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") public int getTransferStatus();
method public boolean isGroupDisabled();
}
@@ -14779,6 +14781,7 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDefaultVoiceSubscriptionId(int);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setPreferredDataSubscriptionId(int, boolean, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<java.lang.Integer>);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setSubscriptionEnabled(int, boolean);
+ method @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void setTransferStatus(int, int);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUiccApplicationsEnabled(int, boolean);
field @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS) public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED";
field @NonNull public static final android.net.Uri ADVANCED_CALLING_ENABLED_CONTENT_URI;
@@ -14788,6 +14791,9 @@
field public static final int PROFILE_CLASS_PROVISIONING = 1; // 0x1
field public static final int PROFILE_CLASS_TESTING = 0; // 0x0
field public static final int PROFILE_CLASS_UNSET = -1; // 0xffffffff
+ field @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") public static final int TRANSFER_STATUS_CONVERTED = 2; // 0x2
+ field @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") public static final int TRANSFER_STATUS_NONE = 0; // 0x0
+ field @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") public static final int TRANSFER_STATUS_TRANSFERRED_OUT = 1; // 0x1
field @NonNull public static final android.net.Uri VT_ENABLED_CONTENT_URI;
field @NonNull public static final android.net.Uri WFC_ENABLED_CONTENT_URI;
field @NonNull public static final android.net.Uri WFC_MODE_CONTENT_URI;
@@ -15653,7 +15659,9 @@
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public int getOtaStatus();
method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public java.util.List<java.lang.String> getSupportedCountries();
method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public java.util.List<java.lang.String> getUnsupportedCountries();
+ method @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public boolean isPsimConversionSupported(int);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public boolean isSupportedCountry(@NonNull String);
+ method @FlaggedApi("com.android.internal.telephony.flags.support_psim_to_esim_conversion") @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void setPsimConversionSupportedCarriers(@NonNull java.util.Set<java.lang.Integer>);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void setSupportedCountries(@NonNull java.util.List<java.lang.String>);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void setUnsupportedCountries(@NonNull java.util.List<java.lang.String>);
field @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public static final String ACTION_CONVERT_TO_EMBEDDED_SUBSCRIPTION = "android.telephony.euicc.action.CONVERT_TO_EMBEDDED_SUBSCRIPTION";
@@ -17377,6 +17385,19 @@
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.AntennaPosition> CREATOR;
}
+ @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public class EnableRequestAttributes {
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public boolean isDemoMode();
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public boolean isEmergencyMode();
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public boolean isEnabled();
+ }
+
+ @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final class EnableRequestAttributes.Builder {
+ ctor @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public EnableRequestAttributes.Builder(boolean);
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public android.telephony.satellite.EnableRequestAttributes build();
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public android.telephony.satellite.EnableRequestAttributes.Builder setDemoMode(boolean);
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @NonNull public android.telephony.satellite.EnableRequestAttributes.Builder setEmergencyMode(boolean);
+ }
+
@FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class NtnSignalStrength implements android.os.Parcelable {
ctor @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public NtnSignalStrength(@Nullable android.telephony.satellite.NtnSignalStrength);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public int describeContents();
@@ -17442,10 +17463,11 @@
method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void removeAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestAttachEnabledForCarrier(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestCapabilities(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telephony.satellite.SatelliteCapabilities,android.telephony.satellite.SatelliteManager.SatelliteException>);
- method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestEnabled(boolean, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestEnabled(@NonNull android.telephony.satellite.EnableRequestAttributes, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsAttachEnabledForCarrier(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsCommunicationAllowedForCurrentLocation(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsDemoModeEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsEmergencyModeEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsProvisioned(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void requestIsSupported(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
@@ -17507,6 +17529,7 @@
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_TELEPHONY_STATE = 6; // 0x6
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_BUSY = 22; // 0x16
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_ERROR = 4; // 0x4
+ field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24; // 0x18
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_NETWORK_ERROR = 5; // 0x5
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_NETWORK_TIMEOUT = 17; // 0x11
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_NOT_AUTHORIZED = 19; // 0x13
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index 6c83fd0..8a485d2 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -525,6 +525,10 @@
Avoid field names that are Kotlin hard keywords ("when"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords
+KotlinOperator: android.hardware.camera2.extension.CharacteristicsMap#get(String):
+ Method can be invoked with an indexing operator from Kotlin: `get` (this is usually desirable; just make sure it makes sense for this type of object)
+
+
ListenerLast: android.telephony.satellite.SatelliteManager#stopSatelliteTransmissionUpdates(android.telephony.satellite.SatelliteTransmissionUpdateCallback, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>) parameter #1:
Listeners should always be at end of argument list (method `stopSatelliteTransmissionUpdates`)
ListenerLast: android.telephony.satellite.SatelliteManager#stopSatelliteTransmissionUpdates(android.telephony.satellite.SatelliteTransmissionUpdateCallback, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>) parameter #2:
@@ -685,6 +689,8 @@
Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getApplicationExemptions(String):
Method 'getApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName):
+ Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName):
Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName):
@@ -731,6 +737,8 @@
Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setApplicationExemptions(String, java.util.Set<java.lang.Integer>):
Method 'setApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int):
+ Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceProvisioningConfigApplied():
Method 'setDeviceProvisioningConfigApplied' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setLockTaskFeatures(android.content.ComponentName, int):
@@ -2305,14 +2313,14 @@
New API must be flagged with @FlaggedApi: constructor android.telephony.satellite.SatelliteManager.SatelliteException(int)
UnflaggedApi: android.telephony.satellite.SatelliteManager.SatelliteException#getErrorCode():
New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteManager.SatelliteException.getErrorCode()
-UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback:
- New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteProvisionStateCallback
-UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback#onSatelliteProvisionStateChanged(boolean):
- New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteProvisionStateCallback.onSatelliteProvisionStateChanged(boolean)
UnflaggedApi: android.telephony.satellite.SatelliteModemStateCallback:
New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteModemStateCallback
UnflaggedApi: android.telephony.satellite.SatelliteModemStateCallback#onSatelliteModemStateChanged(int):
New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteModemStateCallback.onSatelliteModemStateChanged(int)
+UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback:
+ New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteProvisionStateCallback
+UnflaggedApi: android.telephony.satellite.SatelliteProvisionStateCallback#onSatelliteProvisionStateChanged(boolean):
+ New API must be flagged with @FlaggedApi: method android.telephony.satellite.SatelliteProvisionStateCallback.onSatelliteProvisionStateChanged(boolean)
UnflaggedApi: android.telephony.satellite.SatelliteTransmissionUpdateCallback:
New API must be flagged with @FlaggedApi: class android.telephony.satellite.SatelliteTransmissionUpdateCallback
UnflaggedApi: android.telephony.satellite.SatelliteTransmissionUpdateCallback#onReceiveDatagramStateChanged(int, int, int):
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 4a048bd..bbd6bde 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -624,6 +624,7 @@
field public static final int OPERATION_SET_APPLICATION_HIDDEN = 15; // 0xf
field public static final int OPERATION_SET_APPLICATION_RESTRICTIONS = 16; // 0x10
field public static final int OPERATION_SET_CAMERA_DISABLED = 31; // 0x1f
+ field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int OPERATION_SET_CONTENT_PROTECTION_POLICY = 41; // 0x29
field public static final int OPERATION_SET_FACTORY_RESET_PROTECTION_POLICY = 32; // 0x20
field public static final int OPERATION_SET_GLOBAL_PRIVATE_DNS = 33; // 0x21
field public static final int OPERATION_SET_KEEP_UNINSTALLED_PACKAGES = 17; // 0x11
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index 5e904ef9..b938f0f 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -673,6 +673,8 @@
Method 'generateKeyPair' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getApplicationExemptions(String):
Method 'getApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#getContentProtectionPolicy(android.content.ComponentName):
+ Method 'getContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getCrossProfileWidgetProviders(android.content.ComponentName):
Method 'getCrossProfileWidgetProviders' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#getLockTaskFeatures(android.content.ComponentName):
@@ -721,6 +723,8 @@
Method 'setAlwaysOnVpnPackage' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setApplicationExemptions(String, java.util.Set<java.lang.Integer>):
Method 'setApplicationExemptions' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.app.admin.DevicePolicyManager#setContentProtectionPolicy(android.content.ComponentName, int):
+ Method 'setContentProtectionPolicy' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceOwner(android.content.ComponentName, int):
Method 'setDeviceOwner' documentation mentions permissions already declared by @RequiresPermission
RequiresPermission: android.app.admin.DevicePolicyManager#setDeviceProvisioningConfigApplied():
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 0a34d36..aa9de81 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -23,6 +23,7 @@
import static android.app.admin.DevicePolicyResources.UNDEFINED;
import static android.graphics.drawable.Icon.TYPE_URI;
import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP;
+import static android.app.Flags.evenlyDividedCallStyleActionLayout;
import static java.util.Objects.requireNonNull;
@@ -3540,15 +3541,12 @@
* Sets the token used for background operations for the pending intents associated with this
* notification.
*
- * This token is automatically set during deserialization for you, you usually won't need to
- * call this unless you want to change the existing token, if any.
- *
* @hide
*/
- public void clearAllowlistToken() {
- mAllowlistToken = null;
+ public void overrideAllowlistToken(IBinder token) {
+ mAllowlistToken = token;
if (publicVersion != null) {
- publicVersion.clearAllowlistToken();
+ publicVersion.overrideAllowlistToken(token);
}
}
@@ -5957,7 +5955,7 @@
// there is enough space to do so (and fall back to the left edge if not).
big.setInt(R.id.actions, "setCollapsibleIndentDimen",
R.dimen.call_notification_collapsible_indent);
- if (CallStyle.USE_NEW_ACTION_LAYOUT) {
+ if (evenlyDividedCallStyleActionLayout()) {
if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) {
Log.d(TAG, "setting evenly divided mode on action list");
}
@@ -6439,7 +6437,7 @@
title = ContrastColorUtil.ensureColorSpanContrast(title, buttonFillColor);
}
final CharSequence label = ensureColorSpanContrast(title, p);
- if (p.mCallStyleActions && CallStyle.USE_NEW_ACTION_LAYOUT) {
+ if (p.mCallStyleActions && evenlyDividedCallStyleActionLayout()) {
if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) {
Log.d(TAG, "new action layout enabled, gluing instead of setting text");
}
@@ -6463,7 +6461,7 @@
button.setColorStateList(R.id.action0, "setButtonBackground",
ColorStateList.valueOf(buttonFillColor));
if (p.mCallStyleActions) {
- if (CallStyle.USE_NEW_ACTION_LAYOUT) {
+ if (evenlyDividedCallStyleActionLayout()) {
if (CallStyle.DEBUG_NEW_ACTION_LAYOUT) {
Log.d(TAG, "new action layout enabled, gluing instead of setting icon");
}
@@ -9600,11 +9598,6 @@
/**
* @hide
*/
- public static final boolean USE_NEW_ACTION_LAYOUT = false;
-
- /**
- * @hide
- */
public static final boolean DEBUG_NEW_ACTION_LAYOUT = true;
/**
diff --git a/core/java/android/app/QueuedWork.java b/core/java/android/app/QueuedWork.java
index 6a114f9..60b61f3 100644
--- a/core/java/android/app/QueuedWork.java
+++ b/core/java/android/app/QueuedWork.java
@@ -27,6 +27,7 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ExponentiallyBucketedHistogram;
import java.util.LinkedList;
@@ -114,6 +115,20 @@
}
/**
+ * Tear down the handler.
+ */
+ @VisibleForTesting
+ public static void resetHandler() {
+ synchronized (sLock) {
+ if (sHandler == null) {
+ return;
+ }
+ sHandler.getLooper().quitSafely();
+ sHandler = null;
+ }
+ }
+
+ /**
* Remove all Messages from the Handler with the given code.
*
* This method intentionally avoids creating the Handler if it doesn't
diff --git a/core/java/android/app/admin/DevicePolicyIdentifiers.java b/core/java/android/app/admin/DevicePolicyIdentifiers.java
index b0bec78..d7aafa0 100644
--- a/core/java/android/app/admin/DevicePolicyIdentifiers.java
+++ b/core/java/android/app/admin/DevicePolicyIdentifiers.java
@@ -22,7 +22,6 @@
import android.app.admin.flags.Flags;
import android.os.UserManager;
-
import java.util.Objects;
/**
@@ -163,6 +162,12 @@
public static final String CROSS_PROFILE_WIDGET_PROVIDER_POLICY = "crossProfileWidgetProvider";
/**
+ * String identifier for {@link DevicePolicyManager#setContentProtectionPolicy}.
+ */
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public static final String CONTENT_PROTECTION_POLICY = "contentProtection";
+
+ /**
* String identifier for {@link DevicePolicyManager#setUsbDataSignalingEnabled}.
*/
@FlaggedApi(Flags.FLAG_POLICY_ENGINE_MIGRATION_V2_ENABLED)
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 86d0125..1ef4346 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -24,6 +24,7 @@
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CAMERA;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CERTIFICATES;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE;
+import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DEFAULT_SMS;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_FACTORY_RESET;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_INPUT_METHODS;
@@ -53,7 +54,6 @@
import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
-import static android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -3825,6 +3825,10 @@
/** @hide */
@TestApi
public static final int OPERATION_UNINSTALL_CA_CERT = 40;
+ /** @hide */
+ @TestApi
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public static final int OPERATION_SET_CONTENT_PROTECTION_POLICY = 41;
private static final String PREFIX_OPERATION = "OPERATION_";
@@ -3869,7 +3873,8 @@
OPERATION_SET_PERMISSION_GRANT_STATE,
OPERATION_SET_PERMISSION_POLICY,
OPERATION_SET_RESTRICTIONS_PROVIDER,
- OPERATION_UNINSTALL_CA_CERT
+ OPERATION_UNINSTALL_CA_CERT,
+ OPERATION_SET_CONTENT_PROTECTION_POLICY
})
@Retention(RetentionPolicy.SOURCE)
public static @interface DevicePolicyOperation {
@@ -4095,15 +4100,15 @@
}
/** Indicates that content protection is not controlled by policy, allowing user to choose. */
- @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0;
- /** Indicates that content protection is controlled and disabled by a policy. */
- @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ /** Indicates that content protection is controlled and disabled by a policy (default). */
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
public static final int CONTENT_PROTECTION_DISABLED = 1;
/** Indicates that content protection is controlled and enabled by a policy. */
- @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
public static final int CONTENT_PROTECTION_ENABLED = 2;
/** @hide */
@@ -4118,6 +4123,86 @@
public @interface ContentProtectionPolicy {}
/**
+ * Sets the content protection policy which controls scanning for deceptive apps.
+ * <p>
+ * This function can only be called by the device owner, a profile owner of an affiliated user
+ * or profile, or the profile owner when no device owner is set or holders of the permission
+ * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CONTENT_PROTECTION}. See
+ * {@link #isAffiliatedUser}.
+ * Any policy set via this method will be cleared if the user becomes unaffiliated.
+ * <p>
+ * After the content protection policy has been set,
+ * {@link PolicyUpdateReceiver#onPolicySetResult(Context, String, Bundle, TargetUser,
+ * PolicyUpdateResult)} will notify the admin on whether the policy was successfully set or not.
+ * This callback will contain:
+ * <ul>
+ * <li> The policy identifier {@link DevicePolicyIdentifiers#CONTENT_PROTECTION_POLICY}
+ * <li> The {@link TargetUser} that this policy relates to
+ * <li> The {@link PolicyUpdateResult}, which will be
+ * {@link PolicyUpdateResult#RESULT_POLICY_SET} if the policy was successfully set or the
+ * reason the policy failed to be set
+ * (e.g. {@link PolicyUpdateResult#RESULT_FAILURE_CONFLICTING_ADMIN_POLICY})
+ * </ul>
+ * If there has been a change to the policy,
+ * {@link PolicyUpdateReceiver#onPolicyChanged(Context, String, Bundle, TargetUser,
+ * PolicyUpdateResult)} will notify the admin of this change. This callback will contain the
+ * same parameters as PolicyUpdateReceiver#onPolicySetResult and the {@link PolicyUpdateResult}
+ * will contain the reason why the policy changed.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Null if the
+ * caller is not a device admin.
+ * @param policy The content protection policy to set. One of {@link
+ * #CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY},
+ * {@link #CONTENT_PROTECTION_DISABLED} or {@link #CONTENT_PROTECTION_ENABLED}.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set or holder of the
+ * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CONTENT_PROTECTION}.
+ * @see #isAffiliatedUser
+ */
+ @RequiresPermission(value = MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional = true)
+ @SupportsCoexistence
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public void setContentProtectionPolicy(
+ @Nullable ComponentName admin, @ContentProtectionPolicy int policy) {
+ throwIfParentInstance("setContentProtectionPolicy");
+ if (mService != null) {
+ try {
+ mService.setContentProtectionPolicy(admin, mContext.getPackageName(), policy);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Returns the current content protection policy.
+ * <p>
+ * The returned policy will be the current resolved policy rather than the policy set by the
+ * calling admin.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Null if the
+ * caller is not a device admin.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set or holder of the
+ * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_CONTENT_PROTECTION}.
+ * @see #isAffiliatedUser
+ * @see #setContentProtectionPolicy
+ */
+ @RequiresPermission(value = MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, conditional = true)
+ @FlaggedApi(android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+ public @ContentProtectionPolicy int getContentProtectionPolicy(@Nullable ComponentName admin) {
+ throwIfParentInstance("getContentProtectionPolicy");
+ if (mService != null) {
+ try {
+ return mService.getContentProtectionPolicy(admin, mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return CONTENT_PROTECTION_DISABLED;
+ }
+
+ /**
* This object is a single place to tack on invalidation and disable calls. All
* binder caches in this class derive from this Config, so all can be invalidated or
* disabled through this Config.
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 575fa4c..efcf563 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -610,4 +610,7 @@
String getFinancedDeviceKioskRoleHolder(String callerPackageName);
void calculateHasIncompatibleAccounts();
+
+ void setContentProtectionPolicy(in ComponentName who, String callerPackageName, int policy);
+ int getContentProtectionPolicy(in ComponentName who, String callerPackageName);
}
diff --git a/core/java/android/app/admin/SecurityLog.java b/core/java/android/app/admin/SecurityLog.java
index ca2e97e..ed1b8ca 100644
--- a/core/java/android/app/admin/SecurityLog.java
+++ b/core/java/android/app/admin/SecurityLog.java
@@ -17,12 +17,14 @@
package android.app.admin;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.TestApi;
+import android.app.admin.flags.Flags;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.os.Build;
@@ -99,6 +101,7 @@
TAG_PACKAGE_INSTALLED,
TAG_PACKAGE_UPDATED,
TAG_PACKAGE_UNINSTALLED,
+ TAG_BACKUP_SERVICE_TOGGLED,
})
public @interface SecurityLogTag {}
@@ -599,6 +602,18 @@
public static final int TAG_PACKAGE_UNINSTALLED = SecurityLogTags.SECURITY_PACKAGE_UNINSTALLED;
/**
+ * Indicates that an admin has enabled or disabled backup service. The log entry contains the
+ * following information about the event encapsulated in an {@link Object} array, accessible
+ * via {@link SecurityEvent#getData()}:
+ * <li> [0] admin package name ({@code String})
+ * <li> [1] admin user ID ({@code Integer})
+ * <li> [2] backup service state ({@code Integer}, 1 for enabled, 0 for disabled)
+ * @see DevicePolicyManager#setBackupServiceEnabled(ComponentName, boolean)
+ */
+ @FlaggedApi(Flags.FLAG_BACKUP_SERVICE_SECURITY_LOG_EVENT_ENABLED)
+ public static final int TAG_BACKUP_SERVICE_TOGGLED =
+ SecurityLogTags.SECURITY_BACKUP_SERVICE_TOGGLED;
+ /**
* Event severity level indicating that the event corresponds to normal workflow.
*/
public static final int LEVEL_INFO = 1;
diff --git a/core/java/android/app/admin/SecurityLogTags.logtags b/core/java/android/app/admin/SecurityLogTags.logtags
index e4af8dd..7b3aa7b 100644
--- a/core/java/android/app/admin/SecurityLogTags.logtags
+++ b/core/java/android/app/admin/SecurityLogTags.logtags
@@ -47,4 +47,5 @@
210040 security_bluetooth_disconnection (addr|3),(reason|3)
210041 security_package_installed (package_name|3),(version_code|1),(user_id|1)
210042 security_package_updated (package_name|3),(version_code|1),(user_id|1)
-210043 security_package_uninstalled (package_name|3),(version_code|1),(user_id|1)
\ No newline at end of file
+210043 security_package_uninstalled (package_name|3),(version_code|1),(user_id|1)
+210044 security_backup_service_toggled (package|3),(admin_user|1),(enabled|1)
\ No newline at end of file
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index b3ecd92..561eb00 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -62,3 +62,10 @@
description: "Exempt the default sms app of the context user for suspension when calling setPersonalAppsSuspended"
bug: "309183330"
}
+
+flag {
+ name: "backup_service_security_log_event_enabled"
+ namespace: "enterprise"
+ description: "Emit a security log event when DPM.setBackupServiceEnabled is called"
+ bug: "304999634"
+}
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index c40b23e..274d02a 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -56,4 +56,14 @@
namespace: "systemui"
description: "This flag enables the API to allow setting VibrationEffect for NotificationChannels"
bug: "241732519"
+}
+
+flag {
+ name: "evenly_divided_call_style_action_layout"
+ namespace: "systemui"
+ description: "Evenly divides horizontal space for action buttons in CallStyle notifications."
+ bug: "268733030"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/core/java/android/app/wearable/IWearableSensingManager.aidl b/core/java/android/app/wearable/IWearableSensingManager.aidl
index ff37bd8..9d55ce28 100644
--- a/core/java/android/app/wearable/IWearableSensingManager.aidl
+++ b/core/java/android/app/wearable/IWearableSensingManager.aidl
@@ -28,6 +28,8 @@
*/
interface IWearableSensingManager {
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
+ void provideWearableConnection(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback);
diff --git a/core/java/android/app/wearable/WearableSensingManager.java b/core/java/android/app/wearable/WearableSensingManager.java
index eca0039..401d0b7 100644
--- a/core/java/android/app/wearable/WearableSensingManager.java
+++ b/core/java/android/app/wearable/WearableSensingManager.java
@@ -26,6 +26,7 @@
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.app.ambientcontext.AmbientContextEvent;
+import android.companion.CompanionDeviceManager;
import android.content.Context;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
@@ -36,6 +37,8 @@
import android.service.wearable.WearableSensingService;
import android.system.OsConstants;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;
@@ -107,6 +110,14 @@
@FlaggedApi(Flags.FLAG_ENABLE_UNSUPPORTED_OPERATION_STATUS_CODE)
public static final int STATUS_UNSUPPORTED_OPERATION = 6;
+ /**
+ * The value of the status code that indicates an error occurred in the encrypted channel backed
+ * by the provided connection. See {@link #provideWearableConnection(ParcelFileDescriptor,
+ * Executor, Consumer)}.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API)
+ public static final int STATUS_CHANNEL_ERROR = 7;
+
/** @hide */
@IntDef(prefix = { "STATUS_" }, value = {
STATUS_UNKNOWN,
@@ -115,7 +126,8 @@
STATUS_SERVICE_UNAVAILABLE,
STATUS_WEARABLE_UNAVAILABLE,
STATUS_ACCESS_DENIED,
- STATUS_UNSUPPORTED_OPERATION
+ STATUS_UNSUPPORTED_OPERATION,
+ STATUS_CHANNEL_ERROR
})
@Retention(RetentionPolicy.SOURCE)
public @interface StatusCode {}
@@ -132,6 +144,60 @@
}
/**
+ * Provides a remote wearable device connection to the WearableSensingService and sends the
+ * resulting status to the {@code statusConsumer} after the call.
+ *
+ * <p>This is used by applications that will also provide an implementation of the isolated
+ * WearableSensingService.
+ *
+ * <p>The provided {@code wearableConnection} is expected to be a connection to a remotely
+ * connected wearable device. This {@code wearableConnection} will be attached to
+ * CompanionDeviceManager via {@link CompanionDeviceManager#attachSystemDataTransport(int,
+ * InputStream, OutputStream)}, which will create an encrypted channel using {@code
+ * wearableConnection} as the raw underlying connection. The wearable device is expected to
+ * attach its side of the raw connection to its CompanionDeviceManager via the same method so
+ * that the two CompanionDeviceManagers on the two devices can perform attestation and set up
+ * the encrypted channel. Attestation requirements are listed in
+ * com.android.server.security.AttestationVerificationPeerDeviceVerifier
+ *
+ * <p>A proxy to the encrypted channel will be provided to the WearableSensingService, which is
+ * referred to as the secureWearableConnection in WearableSensingService. Any data written to
+ * secureWearableConnection will be encrypted by CompanionDeviceManager and sent over the raw
+ * {@code wearableConnection} to the remote wearable device, which is expected to use its
+ * CompanionDeviceManager to decrypt the data. Encrypted data arriving at the raw {@code
+ * wearableConnection} will be decrypted by CompanionDeviceManager and be readable as plain text
+ * from secureWearableConnection. The raw {@code wearableConnection} provided to this method
+ * will not be directly available to the WearableSensingService.
+ *
+ * <p>If an error occurred in the encrypted channel (such as the underlying stream closed), the
+ * system will send a status code of {@link STATUS_CHANNEL_ERROR} to the {@code statusConsumer}
+ * and kill the WearableSensingService process.
+ *
+ * <p>Before providing the secureWearableConnection, the system will restart the
+ * WearableSensingService process. Other method calls into WearableSensingService may be dropped
+ * during the restart. The caller is responsible for ensuring other method calls are queued
+ * until a success status is returned from the {@code statusConsumer}.
+ *
+ * @param wearableConnection The connection to provide
+ * @param executor Executor on which to run the consumer callback
+ * @param statusConsumer A consumer that handles the status codes for providing the connection
+ * and errors in the encrypted channel.
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)
+ @FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API)
+ public void provideWearableConnection(
+ @NonNull ParcelFileDescriptor wearableConnection,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull @StatusCode Consumer<Integer> statusConsumer) {
+ try {
+ RemoteCallback callback = createStatusCallback(executor, statusConsumer);
+ mService.provideWearableConnection(wearableConnection, callback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Provides a data stream to the WearableSensingService that's backed by the
* parcelFileDescriptor, and sends the result to the {@link Consumer} right after the call.
* This is used by applications that will also provide an implementation of
@@ -149,15 +215,7 @@
@NonNull @CallbackExecutor Executor executor,
@NonNull @StatusCode Consumer<Integer> statusConsumer) {
try {
- RemoteCallback callback = new RemoteCallback(result -> {
- int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY);
- final long identity = Binder.clearCallingIdentity();
- try {
- executor.execute(() -> statusConsumer.accept(status));
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- });
+ RemoteCallback callback = createStatusCallback(executor, statusConsumer);
mService.provideDataStream(parcelFileDescriptor, callback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -191,19 +249,24 @@
@NonNull @CallbackExecutor Executor executor,
@NonNull @StatusCode Consumer<Integer> statusConsumer) {
try {
- RemoteCallback callback = new RemoteCallback(result -> {
- int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY);
- final long identity = Binder.clearCallingIdentity();
- try {
- executor.execute(() -> statusConsumer.accept(status));
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- });
+ RemoteCallback callback = createStatusCallback(executor, statusConsumer);
mService.provideData(data, sharedMemory, callback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
+ private static RemoteCallback createStatusCallback(
+ Executor executor, Consumer<Integer> statusConsumer) {
+ return new RemoteCallback(
+ result -> {
+ int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() -> statusConsumer.accept(status));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ });
+ }
}
diff --git a/core/java/android/content/pm/IBackgroundInstallControlService.aidl b/core/java/android/content/pm/IBackgroundInstallControlService.aidl
index c8e7cae..2e7f19e 100644
--- a/core/java/android/content/pm/IBackgroundInstallControlService.aidl
+++ b/core/java/android/content/pm/IBackgroundInstallControlService.aidl
@@ -17,10 +17,15 @@
package android.content.pm;
import android.content.pm.ParceledListSlice;
+import android.os.IRemoteCallback;
/**
* {@hide}
*/
interface IBackgroundInstallControlService {
ParceledListSlice getBackgroundInstalledPackages(long flags, int userId);
+
+ void registerBackgroundInstallCallback(IRemoteCallback callback);
+
+ void unregisterBackgroundInstallCallback(IRemoteCallback callback);
}
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 08f1853..bff90f1 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -846,4 +846,6 @@
@EnforcePermission("GET_APP_METADATA")
int getAppMetadataSource(String packageName, int userId);
+
+ ComponentName getDomainVerificationAgent();
}
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index f54b2ac..d347a0e 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -16,9 +16,6 @@
package android.content.pm;
-import static android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES;
-
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -476,22 +473,26 @@
)
public @interface ProfileApiVisibility {
}
- /*
- * The api visibility value for this profile user is undefined or unknown.
+
+ /**
+ * The api visibility value for this profile user is undefined or unknown.
+ *
+ * @hide
*/
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public static final int PROFILE_API_VISIBILITY_UNKNOWN = -1;
/**
* Indicates that information about this profile user should be shown in API surfaces.
+ *
+ * @hide
*/
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public static final int PROFILE_API_VISIBILITY_VISIBLE = 0;
/**
* Indicates that information about this profile should be not be visible in API surfaces.
+ *
+ * @hide
*/
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public static final int PROFILE_API_VISIBILITY_HIDDEN = 1;
@@ -555,9 +556,7 @@
setShowInQuietMode(orig.getShowInQuietMode());
setShowInSharingSurfaces(orig.getShowInSharingSurfaces());
setCrossProfileContentSharingStrategy(orig.getCrossProfileContentSharingStrategy());
- if (android.multiuser.Flags.supportHidingProfiles()) {
- setProfileApiVisibility(orig.getProfileApiVisibility());
- }
+ setProfileApiVisibility(orig.getProfileApiVisibility());
}
/**
@@ -1002,9 +1001,10 @@
/**
* Returns the visibility of the profile user in API surfaces. Any information linked to the
* profile (userId, package names) should be hidden API surfaces if a user is marked as hidden.
+ *
+ * @hide
*/
@NonNull
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public @ProfileApiVisibility int getProfileApiVisibility() {
if (isPresent(INDEX_PROFILE_API_VISIBILITY)) return mProfileApiVisibility;
if (mDefaultProperties != null) return mDefaultProperties.mProfileApiVisibility;
@@ -1012,7 +1012,6 @@
}
/** @hide */
@NonNull
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public void setProfileApiVisibility(@ProfileApiVisibility int profileApiVisibility) {
this.mProfileApiVisibility = profileApiVisibility;
setPresent(INDEX_PROFILE_API_VISIBILITY);
@@ -1053,9 +1052,6 @@
@Override
public String toString() {
- String profileApiVisibility =
- android.multiuser.Flags.supportHidingProfiles() ? ", mProfileApiVisibility="
- + getProfileApiVisibility() : "";
// Please print in increasing order of PropertyIndex.
return "UserProperties{"
+ "mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent)
@@ -1079,7 +1075,7 @@
+ ", mDeleteAppWithParent=" + getDeleteAppWithParent()
+ ", mAlwaysVisible=" + getAlwaysVisible()
+ ", mCrossProfileContentSharingStrategy=" + getCrossProfileContentSharingStrategy()
- + ", mProfileApiVisibility=" + profileApiVisibility
+ + ", mProfileApiVisibility=" + getProfileApiVisibility()
+ ", mItemsRestrictedOnHomeScreen=" + areItemsRestrictedOnHomeScreen()
+ "}";
}
@@ -1114,9 +1110,7 @@
pw.println(prefix + " mAlwaysVisible=" + getAlwaysVisible());
pw.println(prefix + " mCrossProfileContentSharingStrategy="
+ getCrossProfileContentSharingStrategy());
- if (android.multiuser.Flags.supportHidingProfiles()) {
- pw.println(prefix + " mProfileApiVisibility=" + getProfileApiVisibility());
- }
+ pw.println(prefix + " mProfileApiVisibility=" + getProfileApiVisibility());
pw.println(prefix + " mItemsRestrictedOnHomeScreen=" + areItemsRestrictedOnHomeScreen());
}
@@ -1203,9 +1197,7 @@
setCrossProfileContentSharingStrategy(parser.getAttributeInt(i));
break;
case ATTR_PROFILE_API_VISIBILITY:
- if (android.multiuser.Flags.supportHidingProfiles()) {
- setProfileApiVisibility(parser.getAttributeInt(i));
- }
+ setProfileApiVisibility(parser.getAttributeInt(i));
break;
case ITEMS_RESTRICTED_ON_HOME_SCREEN:
setItemsRestrictedOnHomeScreen(parser.getAttributeBoolean(i));
@@ -1293,10 +1285,8 @@
mCrossProfileContentSharingStrategy);
}
if (isPresent(INDEX_PROFILE_API_VISIBILITY)) {
- if (android.multiuser.Flags.supportHidingProfiles()) {
- serializer.attributeInt(null, ATTR_PROFILE_API_VISIBILITY,
- mProfileApiVisibility);
- }
+ serializer.attributeInt(null, ATTR_PROFILE_API_VISIBILITY,
+ mProfileApiVisibility);
}
if (isPresent(INDEX_ITEMS_RESTRICTED_ON_HOME_SCREEN)) {
serializer.attributeBoolean(null, ITEMS_RESTRICTED_ON_HOME_SCREEN,
@@ -1566,7 +1556,6 @@
* @hide
*/
@NonNull
- @FlaggedApi(FLAG_SUPPORT_HIDING_PROFILES)
public Builder setProfileApiVisibility(@ProfileApiVisibility int profileApiVisibility){
mProfileApiVisibility = profileApiVisibility;
return this;
@@ -1650,9 +1639,7 @@
setDeleteAppWithParent(deleteAppWithParent);
setAlwaysVisible(alwaysVisible);
setCrossProfileContentSharingStrategy(crossProfileContentSharingStrategy);
- if (android.multiuser.Flags.supportHidingProfiles()) {
- setProfileApiVisibility(profileApiVisibility);
- }
+ setProfileApiVisibility(profileApiVisibility);
setItemsRestrictedOnHomeScreen(itemsRestrictedOnHomeScreen);
}
}
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index f31521d..5e9d8f0 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -206,3 +206,11 @@
description: "Feature flag to enable pre-verified domains"
bug: "307327678"
}
+
+flag {
+ name: "min_target_sdk_24"
+ namespace: "responsible_apis"
+ description: "Feature flag to bump min target sdk to 24"
+ bug: "297603927"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 7ded747..4b890fa 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -109,18 +109,10 @@
}
flag {
- name: "allow_private_profile_apis"
+ name: "enable_private_space_features"
namespace: "profile_experiences"
- description: "Enable only the API changes to support private space"
- bug: "299069460"
-}
-
-flag {
- name: "support_hiding_profiles"
- namespace: "profile_experiences"
- description: "Allow the use of a hide_profile property to hide some profiles behind a permission"
- bug: "316362775"
- is_fixed_read_only: true
+ description: "Enable the support for private space and all its sub-features"
+ bug: "286418785"
}
flag {
diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationState.java b/core/java/android/content/pm/verify/domain/DomainVerificationState.java
index 8e28042..6c4fe37 100644
--- a/core/java/android/content/pm/verify/domain/DomainVerificationState.java
+++ b/core/java/android/content/pm/verify/domain/DomainVerificationState.java
@@ -33,7 +33,8 @@
STATE_DENIED,
STATE_LEGACY_FAILURE,
STATE_SYS_CONFIG,
- STATE_FIRST_VERIFIER_DEFINED
+ STATE_PRE_VERIFIED,
+ STATE_FIRST_VERIFIER_DEFINED,
})
@interface State {
}
@@ -92,6 +93,13 @@
int STATE_SYS_CONFIG = 7;
/**
+ * The application has temporarily been granted auto verification for a set of domains as
+ * specified by a trusted installer during the installation. This will treat the domain as
+ * verified, but it should be updated by the verification agent.
+ */
+ int STATE_PRE_VERIFIED = 8;
+
+ /**
* @see DomainVerificationInfo#STATE_FIRST_VERIFIER_DEFINED
*/
int STATE_FIRST_VERIFIER_DEFINED = 0b10000000000;
@@ -115,6 +123,8 @@
return "legacy_failure";
case DomainVerificationState.STATE_SYS_CONFIG:
return "system_configured";
+ case DomainVerificationState.STATE_PRE_VERIFIED:
+ return "pre_verified";
default:
return String.valueOf(state);
}
@@ -135,6 +145,7 @@
case STATE_DENIED:
case STATE_LEGACY_FAILURE:
case STATE_SYS_CONFIG:
+ case STATE_PRE_VERIFIED:
default:
return false;
}
@@ -151,6 +162,7 @@
case DomainVerificationState.STATE_MIGRATED:
case DomainVerificationState.STATE_RESTORED:
case DomainVerificationState.STATE_SYS_CONFIG:
+ case DomainVerificationState.STATE_PRE_VERIFIED:
return true;
case DomainVerificationState.STATE_NO_RESPONSE:
case DomainVerificationState.STATE_DENIED:
@@ -173,6 +185,7 @@
case DomainVerificationState.STATE_MIGRATED:
case DomainVerificationState.STATE_RESTORED:
case DomainVerificationState.STATE_LEGACY_FAILURE:
+ case DomainVerificationState.STATE_PRE_VERIFIED:
return true;
case DomainVerificationState.STATE_APPROVED:
case DomainVerificationState.STATE_DENIED:
@@ -194,6 +207,7 @@
case STATE_RESTORED:
case STATE_APPROVED:
case STATE_DENIED:
+ case STATE_PRE_VERIFIED:
return true;
case STATE_NO_RESPONSE:
case STATE_LEGACY_FAILURE:
diff --git a/core/java/android/credentials/Constants.java b/core/java/android/credentials/Constants.java
new file mode 100644
index 0000000..ea30c44
--- /dev/null
+++ b/core/java/android/credentials/Constants.java
@@ -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 android.credentials;
+
+/**
+ * Constants for credential manager service that doesn't fit into other structures
+ *
+ * @hide
+ */
+public class Constants {
+ /**
+ * The request is success and user selected an entry
+ */
+ public static final int SUCCESS_CREDMAN_SELECTOR = 0;
+ /**
+ * The error code for ui getting cancelled by user
+ */
+ public static final int FAILURE_CREDMAN_SELECTOR = -1;
+}
diff --git a/core/java/android/credentials/GetCandidateCredentialsResponse.java b/core/java/android/credentials/GetCandidateCredentialsResponse.java
index 73361ad..6e53fd9 100644
--- a/core/java/android/credentials/GetCandidateCredentialsResponse.java
+++ b/core/java/android/credentials/GetCandidateCredentialsResponse.java
@@ -41,8 +41,6 @@
private final PendingIntent mPendingIntent;
- private final GetCredentialResponse mGetCredentialResponse;
-
/**
* @hide
*/
@@ -52,7 +50,6 @@
) {
mCandidateProviderDataList = null;
mPendingIntent = null;
- mGetCredentialResponse = getCredentialResponse;
}
/**
@@ -68,7 +65,6 @@
/*valueName=*/ "candidateProviderDataList");
mCandidateProviderDataList = new ArrayList<>(candidateProviderDataList);
mPendingIntent = pendingIntent;
- mGetCredentialResponse = null;
}
/**
@@ -85,15 +81,6 @@
*
* @hide
*/
- public GetCredentialResponse getGetCredentialResponse() {
- return mGetCredentialResponse;
- }
-
- /**
- * Returns candidate provider data list.
- *
- * @hide
- */
public PendingIntent getPendingIntent() {
return mPendingIntent;
}
@@ -106,14 +93,12 @@
AnnotationValidations.validate(NonNull.class, null, mCandidateProviderDataList);
mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
- mGetCredentialResponse = in.readTypedObject(GetCredentialResponse.CREATOR);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeTypedList(mCandidateProviderDataList);
dest.writeTypedObject(mPendingIntent, flags);
- dest.writeTypedObject(mGetCredentialResponse, flags);
}
@Override
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 1ca11e6..ec46d2f 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -55,3 +55,10 @@
description: "Enables Credential Manager to work with the Biometric Authenticate API"
bug: "323211850"
}
+
+flag {
+ namespace: "wear_frameworks"
+ name: "wear_credential_manager_enabled"
+ description: "Enables Credential Manager on Wear Platform"
+ bug: "301168341"
+}
diff --git a/core/java/android/credentials/selection/Constants.java b/core/java/android/credentials/selection/Constants.java
index 7e6c781..f7fec23 100644
--- a/core/java/android/credentials/selection/Constants.java
+++ b/core/java/android/credentials/selection/Constants.java
@@ -36,5 +36,11 @@
public static final String EXTRA_REQ_FOR_ALL_OPTIONS =
"android.credentials.selection.extra.REQ_FOR_ALL_OPTIONS";
+ /**
+ * The intent extra key for the final result receiver object
+ */
+ public static final String EXTRA_FINAL_RESPONSE_RECEIVER =
+ "android.credentials.selection.extra.FINAL_RESPONSE_RECEIVER";
+
private Constants() {}
}
diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java
index b9bb059..f3efd89 100644
--- a/core/java/android/os/BatteryManager.java
+++ b/core/java/android/os/BatteryManager.java
@@ -238,6 +238,16 @@
public static final int CHARGING_POLICY_ADAPTIVE_LONGLIFE =
OsProtoEnums.CHARGING_POLICY_ADAPTIVE_LONGLIFE; // = 4
+ /**
+ * Returns true if the policy is some type of adaptive charging policy.
+ * @hide
+ */
+ public static boolean isAdaptiveChargingPolicy(int policy) {
+ return policy == CHARGING_POLICY_ADAPTIVE_AC
+ || policy == CHARGING_POLICY_ADAPTIVE_AON
+ || policy == CHARGING_POLICY_ADAPTIVE_LONGLIFE;
+ }
+
// values for "battery part status" property
/**
* Battery part status is not supported.
diff --git a/core/java/android/os/BatteryManagerInternal.java b/core/java/android/os/BatteryManagerInternal.java
index 9bad0de..0ec8729 100644
--- a/core/java/android/os/BatteryManagerInternal.java
+++ b/core/java/android/os/BatteryManagerInternal.java
@@ -16,6 +16,8 @@
package android.os;
+import android.annotation.NonNull;
+
/**
* Battery manager local system service interface.
*
@@ -84,6 +86,26 @@
*/
public abstract boolean getBatteryLevelLow();
+ public interface ChargingPolicyChangeListener {
+ void onChargingPolicyChanged(int newPolicy);
+ }
+
+ /**
+ * Register a listener for changes to {@link BatteryManager#BATTERY_PROPERTY_CHARGING_POLICY}.
+ * The charging policy can't be added to the BATTERY_CHANGED intent because it requires
+ * the BATTERY_STATS permission.
+ */
+ public abstract void registerChargingPolicyChangeListener(
+ @NonNull ChargingPolicyChangeListener chargingPolicyChangeListener);
+
+ /**
+ * Returns the value of {@link BatteryManager#BATTERY_PROPERTY_CHARGING_POLICY}.
+ * This will return {@link Integer#MIN_VALUE} if the device does not support the property.
+ *
+ * @see BatteryManager#getIntProperty(int)
+ */
+ public abstract int getChargingPolicy();
+
/**
* Returns a non-zero value if an unsupported charger is attached.
*
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index 08b32bf..c9c91fc 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -33,7 +33,6 @@
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
-import android.util.Log;
import android.util.MathUtils;
import com.android.internal.util.Preconditions;
@@ -54,7 +53,6 @@
* <p>These effects may be any number of things, from single shot vibrations to complex waveforms.
*/
public abstract class VibrationEffect implements Parcelable {
- private static final String TAG = "VibrationEffect";
// Stevens' coefficient to scale the perceived vibration intensity.
private static final float SCALE_GAMMA = 0.65f;
// If a vibration is playing for longer than 1s, it's probably not haptic feedback
@@ -397,32 +395,26 @@
return null;
}
- try {
- final ContentResolver cr = context.getContentResolver();
- Uri uncanonicalUri = cr.uncanonicalize(uri);
- if (uncanonicalUri == null) {
- // If we already had an uncanonical URI, it's possible we'll get null back here. In
- // this case, just use the URI as passed in since it wasn't canonicalized in the
- // first place.
- uncanonicalUri = uri;
- }
+ final ContentResolver cr = context.getContentResolver();
+ Uri uncanonicalUri = cr.uncanonicalize(uri);
+ if (uncanonicalUri == null) {
+ // If we already had an uncanonical URI, it's possible we'll get null back here. In
+ // this case, just use the URI as passed in since it wasn't canonicalized in the first
+ // place.
+ uncanonicalUri = uri;
+ }
- for (int i = 0; i < uris.length && i < RINGTONES.length; i++) {
- if (uris[i] == null) {
- continue;
- }
- Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i]));
- if (mappedUri == null) {
- continue;
- }
- if (mappedUri.equals(uncanonicalUri)) {
- return get(RINGTONES[i]);
- }
+ for (int i = 0; i < uris.length && i < RINGTONES.length; i++) {
+ if (uris[i] == null) {
+ continue;
}
- } catch (Exception e) {
- // Don't give unexpected exceptions to callers if the Uri's ContentProvider is
- // misbehaving - it's very unlikely to be mapped in that case anyway.
- Log.e(TAG, "Exception getting default vibration for Uri " + uri, e);
+ Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i]));
+ if (mappedUri == null) {
+ continue;
+ }
+ if (mappedUri.equals(uncanonicalUri)) {
+ return get(RINGTONES[i]);
+ }
}
return null;
}
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 6c728a4..abfa4e3 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -122,3 +122,11 @@
is_fixed_read_only: true
bug: "309792384"
}
+
+flag {
+ namespace: "system_performance"
+ name: "telemetry_apis_framework_initialization"
+ description: "Control framework initialization APIs of telemetry APIs feature."
+ is_fixed_read_only: true
+ bug: "324241334"
+}
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index 437668c..ea9375e 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -16,13 +16,6 @@
flag {
namespace: "haptics"
- name: "haptics_customization_ringtone_v2_enabled"
- description: "Enables the usage of the new RingtoneV2 class"
- bug: "241918098"
-}
-
-flag {
- namespace: "haptics"
name: "enable_vibration_serialization_apis"
description: "Enables the APIs for vibration serialization/deserialization."
bug: "245129509"
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index 2841dc0..658cec8 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -4943,6 +4943,13 @@
public static final String COLUMN_IS_NTN = "is_ntn";
/**
+ * TelephonyProvider column name for transferred status
+ *
+ * @hide
+ */
+ public static final String COLUMN_TRANSFER_STATUS = "transfer_status";
+
+ /**
* TelephonyProvider column name to indicate the service capability bitmasks.
*
* @hide
@@ -5021,7 +5028,8 @@
COLUMN_SATELLITE_ENABLED,
COLUMN_SATELLITE_ATTACH_ENABLED_FOR_CARRIER,
COLUMN_IS_NTN,
- COLUMN_SERVICE_CAPABILITIES
+ COLUMN_SERVICE_CAPABILITIES,
+ COLUMN_TRANSFER_STATUS
);
/**
diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java
index 1afe8d9..da8817a 100644
--- a/core/java/android/service/autofill/Dataset.java
+++ b/core/java/android/service/autofill/Dataset.java
@@ -26,8 +26,8 @@
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.content.ClipData;
+import android.content.Intent;
import android.content.IntentSender;
-import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;
@@ -190,7 +190,7 @@
@Nullable private final InlinePresentation mInlineTooltipPresentation;
private final IntentSender mAuthentication;
- @Nullable private final Bundle mAuthenticationExtras;
+ @Nullable private Intent mCredentialFillInIntent;
@Nullable String mId;
@@ -229,7 +229,7 @@
mInlinePresentation = inlinePresentation;
mInlineTooltipPresentation = inlineTooltipPresentation;
mAuthentication = authentication;
- mAuthenticationExtras = null;
+ mCredentialFillInIntent = null;
mId = id;
}
@@ -252,7 +252,7 @@
mInlinePresentation = dataset.mInlinePresentation;
mInlineTooltipPresentation = dataset.mInlineTooltipPresentation;
mAuthentication = dataset.mAuthentication;
- mAuthenticationExtras = dataset.mAuthenticationExtras;
+ mCredentialFillInIntent = dataset.mCredentialFillInIntent;
mId = dataset.mId;
mAutofillDatatypes = dataset.mAutofillDatatypes;
}
@@ -271,7 +271,7 @@
mInlinePresentation = builder.mInlinePresentation;
mInlineTooltipPresentation = builder.mInlineTooltipPresentation;
mAuthentication = builder.mAuthentication;
- mAuthenticationExtras = builder.mAuthenticationExtras;
+ mCredentialFillInIntent = builder.mCredentialFillInIntent;
mId = builder.mId;
mAutofillDatatypes = builder.mAutofillDatatypes;
}
@@ -354,8 +354,14 @@
/** @hide */
@Hide
- public @Nullable Bundle getAuthenticationExtras() {
- return mAuthenticationExtras;
+ public @Nullable Intent getCredentialFillInIntent() {
+ return mCredentialFillInIntent;
+ }
+
+ /** @hide */
+ @Hide
+ public void setCredentialFillInIntent(Intent intent) {
+ mCredentialFillInIntent = intent;
}
/** @hide */
@@ -415,7 +421,7 @@
if (mAuthentication != null) {
builder.append(", hasAuthentication");
}
- if (mAuthenticationExtras != null) {
+ if (mCredentialFillInIntent != null) {
builder.append(", hasAuthenticationExtras");
}
if (mAutofillDatatypes != null) {
@@ -472,7 +478,7 @@
@Nullable private InlinePresentation mInlineTooltipPresentation;
private IntentSender mAuthentication;
- private Bundle mAuthenticationExtras;
+ private Intent mCredentialFillInIntent;
private boolean mDestroyed;
@Nullable private String mId;
@@ -655,9 +661,9 @@
* @hide
*/
@Hide
- public @NonNull Builder setAuthenticationExtras(@Nullable Bundle authenticationExtra) {
+ public @NonNull Builder setCredentialFillInIntent(@Nullable Intent credentialFillInIntent) {
throwIfDestroyed();
- mAuthenticationExtras = authenticationExtra;
+ mCredentialFillInIntent = credentialFillInIntent;
return this;
}
@@ -1401,7 +1407,7 @@
parcel.writeParcelable(mAuthentication, flags);
parcel.writeString(mId);
parcel.writeInt(mEligibleReason);
- parcel.writeTypedObject(mAuthenticationExtras, flags);
+ parcel.writeTypedObject(mCredentialFillInIntent, flags);
}
public static final @NonNull Creator<Dataset> CREATOR = new Creator<Dataset>() {
@@ -1437,7 +1443,7 @@
android.content.IntentSender.class);
final String datasetId = parcel.readString();
final int eligibleReason = parcel.readInt();
- final Bundle authenticationExtras = parcel.readTypedObject(Bundle.CREATOR);
+ final Intent credentialFillInIntent = parcel.readTypedObject(Intent.CREATOR);
// Always go through the builder to ensure the data ingested by
// the system obeys the contract of the builder to avoid attacks
@@ -1482,7 +1488,7 @@
fieldDialogPresentation);
}
builder.setAuthentication(authentication);
- builder.setAuthenticationExtras(authenticationExtras);
+ builder.setCredentialFillInIntent(credentialFillInIntent);
builder.setId(datasetId);
Dataset dataset = builder.build();
dataset.mEligibleReason = eligibleReason;
diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java
index 09ec933..c43ba6c 100644
--- a/core/java/android/service/autofill/FillResponse.java
+++ b/core/java/android/service/autofill/FillResponse.java
@@ -56,6 +56,7 @@
* <p>See the main {@link AutofillService} documentation for more details and examples.
*/
public final class FillResponse implements Parcelable {
+ // common_typos_disable
/**
* Flag used to generate {@link FillEventHistory.Event events} of type
@@ -82,11 +83,17 @@
*/
public static final int FLAG_DELAY_FILL = 0x4;
+ /**
+ * @hide
+ */
+ public static final int FLAG_CREDENTIAL_MANAGER_RESPONSE = 0x8;
+
/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_TRACK_CONTEXT_COMMITED,
FLAG_DISABLE_ACTIVITY_ONLY,
- FLAG_DELAY_FILL
+ FLAG_DELAY_FILL,
+ FLAG_CREDENTIAL_MANAGER_RESPONSE
})
@Retention(RetentionPolicy.SOURCE)
@interface FillResponseFlags {}
@@ -834,7 +841,9 @@
public Builder setFlags(@FillResponseFlags int flags) {
throwIfDestroyed();
mFlags = Preconditions.checkFlagsArgument(flags,
- FLAG_TRACK_CONTEXT_COMMITED | FLAG_DISABLE_ACTIVITY_ONLY | FLAG_DELAY_FILL);
+ FLAG_TRACK_CONTEXT_COMMITED
+ | FLAG_DISABLE_ACTIVITY_ONLY | FLAG_DELAY_FILL
+ | FLAG_CREDENTIAL_MANAGER_RESPONSE);
return this;
}
diff --git a/core/java/android/service/wearable/IWearableSensingService.aidl b/core/java/android/service/wearable/IWearableSensingService.aidl
index 44a13c4..8fdb2c2 100644
--- a/core/java/android/service/wearable/IWearableSensingService.aidl
+++ b/core/java/android/service/wearable/IWearableSensingService.aidl
@@ -28,6 +28,7 @@
* @hide
*/
oneway interface IWearableSensingService {
+ void provideSecureWearableConnection(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback);
void startDetection(in AmbientContextEventRequest request, in String packageName,
diff --git a/core/java/android/service/wearable/WearableSensingService.java b/core/java/android/service/wearable/WearableSensingService.java
index e7e44a5..d21115b 100644
--- a/core/java/android/service/wearable/WearableSensingService.java
+++ b/core/java/android/service/wearable/WearableSensingService.java
@@ -17,12 +17,14 @@
package android.service.wearable;
import android.annotation.BinderThread;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.Service;
import android.app.ambientcontext.AmbientContextEvent;
import android.app.ambientcontext.AmbientContextEventRequest;
+import android.app.wearable.Flags;
import android.app.wearable.WearableSensingManager;
import android.content.Intent;
import android.os.Bundle;
@@ -39,6 +41,7 @@
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
@@ -94,17 +97,20 @@
return new IWearableSensingService.Stub() {
/** {@inheritDoc} */
@Override
+ public void provideSecureWearableConnection(
+ ParcelFileDescriptor secureWearableConnection, RemoteCallback callback) {
+ Objects.requireNonNull(secureWearableConnection);
+ Consumer<Integer> consumer = createWearableStatusConsumer(callback);
+ WearableSensingService.this.onSecureWearableConnectionProvided(
+ secureWearableConnection, consumer);
+ }
+
+ /** {@inheritDoc} */
+ @Override
public void provideDataStream(
- ParcelFileDescriptor parcelFileDescriptor,
- RemoteCallback callback) {
+ ParcelFileDescriptor parcelFileDescriptor, RemoteCallback callback) {
Objects.requireNonNull(parcelFileDescriptor);
- Consumer<Integer> consumer = response -> {
- Bundle bundle = new Bundle();
- bundle.putInt(
- STATUS_RESPONSE_BUNDLE_KEY,
- response);
- callback.sendResult(bundle);
- };
+ Consumer<Integer> consumer = createWearableStatusConsumer(callback);
WearableSensingService.this.onDataStreamProvided(
parcelFileDescriptor, consumer);
}
@@ -116,38 +122,38 @@
SharedMemory sharedMemory,
RemoteCallback callback) {
Objects.requireNonNull(data);
- Consumer<Integer> consumer = response -> {
- Bundle bundle = new Bundle();
- bundle.putInt(
- STATUS_RESPONSE_BUNDLE_KEY,
- response);
- callback.sendResult(bundle);
- };
+ Consumer<Integer> consumer = createWearableStatusConsumer(callback);
WearableSensingService.this.onDataProvided(data, sharedMemory, consumer);
}
/** {@inheritDoc} */
@Override
- public void startDetection(@NonNull AmbientContextEventRequest request,
- String packageName, RemoteCallback detectionResultCallback,
+ public void startDetection(
+ @NonNull AmbientContextEventRequest request,
+ String packageName,
+ RemoteCallback detectionResultCallback,
RemoteCallback statusCallback) {
Objects.requireNonNull(request);
Objects.requireNonNull(packageName);
Objects.requireNonNull(detectionResultCallback);
Objects.requireNonNull(statusCallback);
- Consumer<AmbientContextDetectionResult> detectionResultConsumer = result -> {
- Bundle bundle = new Bundle();
- bundle.putParcelable(
- AmbientContextDetectionResult.RESULT_RESPONSE_BUNDLE_KEY, result);
- detectionResultCallback.sendResult(bundle);
- };
- Consumer<AmbientContextDetectionServiceStatus> statusConsumer = status -> {
- Bundle bundle = new Bundle();
- bundle.putParcelable(
- AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY,
- status);
- statusCallback.sendResult(bundle);
- };
+ Consumer<AmbientContextDetectionResult> detectionResultConsumer =
+ result -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ AmbientContextDetectionResult.RESULT_RESPONSE_BUNDLE_KEY,
+ result);
+ detectionResultCallback.sendResult(bundle);
+ };
+ Consumer<AmbientContextDetectionServiceStatus> statusConsumer =
+ status -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ AmbientContextDetectionServiceStatus
+ .STATUS_RESPONSE_BUNDLE_KEY,
+ status);
+ statusCallback.sendResult(bundle);
+ };
WearableSensingService.this.onStartDetection(
request, packageName, statusConsumer, detectionResultConsumer);
Slog.d(TAG, "startDetection " + request);
@@ -162,23 +168,26 @@
/** {@inheritDoc} */
@Override
- public void queryServiceStatus(@AmbientContextEvent.EventCode int[] eventTypes,
- String packageName, RemoteCallback callback) {
+ public void queryServiceStatus(
+ @AmbientContextEvent.EventCode int[] eventTypes,
+ String packageName,
+ RemoteCallback callback) {
Objects.requireNonNull(eventTypes);
Objects.requireNonNull(packageName);
Objects.requireNonNull(callback);
- Consumer<AmbientContextDetectionServiceStatus> consumer = response -> {
- Bundle bundle = new Bundle();
- bundle.putParcelable(
- AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY,
- response);
- callback.sendResult(bundle);
- };
+ Consumer<AmbientContextDetectionServiceStatus> consumer =
+ response -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ AmbientContextDetectionServiceStatus
+ .STATUS_RESPONSE_BUNDLE_KEY,
+ response);
+ callback.sendResult(bundle);
+ };
Integer[] events = intArrayToIntegerArray(eventTypes);
WearableSensingService.this.onQueryServiceStatus(
new HashSet<>(Arrays.asList(events)), packageName, consumer);
}
-
};
}
Slog.w(TAG, "Incorrect service interface, returning null.");
@@ -186,6 +195,30 @@
}
/**
+ * Called when a secure connection to the wearable is available. See {@link
+ * WearableSensingManager#provideWearableConnection(ParcelFileDescriptor, Executor, Consumer)}
+ * for details about the secure connection.
+ *
+ * <p>When the {@code secureWearableConnection} is closed, the system will send a {@link
+ * WearableSensingManager#STATUS_CHANNEL_ERROR} status code to the status consumer provided by
+ * the caller of {@link WearableSensingManager#provideWearableConnection(ParcelFileDescriptor,
+ * Executor, Consumer)}.
+ *
+ * <p>The implementing class should override this method. It should return an appropriate status
+ * code via {@code statusConsumer} after receiving the {@code secureWearableConnection}.
+ *
+ * @param secureWearableConnection The secure connection to the wearable.
+ * @param statusConsumer The consumer for the service status.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API)
+ @BinderThread
+ public void onSecureWearableConnectionProvided(
+ @NonNull ParcelFileDescriptor secureWearableConnection,
+ @NonNull Consumer<Integer> statusConsumer) {
+ statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION);
+ }
+
+ /**
* Called when a data stream to the wearable is provided. This data stream can be used to obtain
* data from a wearable device. It is up to the implementation to maintain the data stream and
* close the data stream when it is finished.
@@ -275,4 +308,13 @@
}
return intArray;
}
+
+ @NonNull
+ private static Consumer<Integer> createWearableStatusConsumer(RemoteCallback statusCallback) {
+ return response -> {
+ Bundle bundle = new Bundle();
+ bundle.putInt(STATUS_RESPONSE_BUNDLE_KEY, response);
+ statusCallback.sendResult(bundle);
+ };
+ }
}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 1ea80f1..8ddb42d 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -18,7 +18,7 @@
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
-import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION;
+import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
@@ -172,7 +172,7 @@
/**
* Value for justification mode indicating the text is justified by stretching letter spacing.
*/
- @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
public static final int JUSTIFICATION_MODE_INTER_CHARACTER =
LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER;
@@ -1831,7 +1831,7 @@
* @return the number of cluster count in the line.
*/
@IntRange(from = 0)
- @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
public int getLineLetterSpacingUnitCount(@IntRange(from = 0) int line,
boolean includeTrailingWhitespace) {
final int start = getLineStart(line);
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index 224e5d8..bde9c77 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -305,7 +305,7 @@
}
mAddedWordSpacingInPx = (justifyWidth - width) / spaces;
mAddedLetterSpacingInPx = 0;
- } else { // justificationMode == Layout.JUSTIFICATION_MODE_INTER_CHARACTER
+ } else { // justificationMode == Layout.JUSTIFICATION_MODE_LETTER_SPACING
LineInfo lineInfo = new LineInfo();
float width = Math.abs(measure(end, false, null, null, lineInfo));
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index a49aee1..f37c4c2a 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -77,7 +77,7 @@
}
flag {
- name: "inter_character_justification"
+ name: "letter_spacing_justification"
namespace: "text"
description: "A feature flag that implement inter character justification."
bug: "283193133"
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 7bae7ec..4ba4ee3 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -1497,13 +1497,21 @@
* {@link View#SYSTEM_UI_LAYOUT_FLAGS} as well the
* {@link WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE} flag and fits content according
* to these flags.
- * </p>
+ *
* <p>
* If set to {@code false}, the framework will not fit the content view to the insets and will
* just pass through the {@link WindowInsets} to the content view.
- * </p>
+ *
+ * <p>
+ * If the app targets
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ * the behavior will be like setting this to {@code false}, and cannot be changed.
+ *
* @param decorFitsSystemWindows Whether the decor view should fit root-level content views for
* insets.
+ * @deprecated Make space in the container views to prevent the critical elements from getting
+ * obscured by {@link WindowInsets.Type#systemBars()} or
+ * {@link WindowInsets.Type#displayCutout()} instead.
*/
public void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) {
}
@@ -2597,7 +2605,9 @@
/**
* @return the color of the status bar.
+ * @deprecated This is no longer needed since the setter is deprecated.
*/
+ @Deprecated
@ColorInt
public abstract int getStatusBarColor();
@@ -2614,13 +2624,29 @@
* {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.
* <p>
* The transitionName for the view background will be "android:status:background".
- * </p>
+ *
+ * <p>
+ * If the color is transparent and the window enforces the status bar contrast, the system
+ * will determine whether a scrim is necessary and draw one on behalf of the app to ensure
+ * that the status bar has enough contrast with the contents of this app, and set an appropriate
+ * effective bar background accordingly.
+ *
+ * <p>
+ * If the app targets
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ * the color will be transparent and cannot be changed.
+ *
+ * @see #setNavigationBarContrastEnforced
+ * @deprecated Draw proper background behind {@link WindowInsets.Type#statusBars()}} instead.
*/
+ @Deprecated
public abstract void setStatusBarColor(@ColorInt int color);
/**
* @return the color of the navigation bar.
+ * @deprecated This is no longer needed since the setter is deprecated.
*/
+ @Deprecated
@ColorInt
public abstract int getNavigationBarColor();
@@ -2637,9 +2663,24 @@
* {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}.
* <p>
* The transitionName for the view background will be "android:navigation:background".
- * </p>
+ *
+ * <p>
+ * If the color is transparent and the window enforces the navigation bar contrast, the system
+ * will determine whether a scrim is necessary and draw one on behalf of the app to ensure that
+ * the navigation bar has enough contrast with the contents of this app, and set an appropriate
+ * effective bar background accordingly.
+ *
+ * <p>
+ * If the app targets
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ * the color will be transparent and cannot be changed.
+ *
* @attr ref android.R.styleable#Window_navigationBarColor
+ * @see #setNavigationBarContrastEnforced
+ * @deprecated Draw proper background behind {@link WindowInsets.Type#navigationBars()} or
+ * {@link WindowInsets.Type#tappableElement()} instead.
*/
+ @Deprecated
public abstract void setNavigationBarColor(@ColorInt int color);
/**
@@ -2651,9 +2692,17 @@
* {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
* {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION} must not be set.
*
+ * <p>
+ * If the app targets
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ * the color will be transparent and cannot be changed.
+ *
* @param dividerColor The color of the thin line.
* @attr ref android.R.styleable#Window_navigationBarDividerColor
+ * @deprecated Draw proper background behind {@link WindowInsets.Type#navigationBars()} or
+ * {@link WindowInsets.Type#tappableElement()} instead.
*/
+ @Deprecated
public void setNavigationBarDividerColor(@ColorInt int dividerColor) {
}
@@ -2663,7 +2712,9 @@
* @return The color of the navigation bar divider color.
* @see #setNavigationBarColor(int)
* @attr ref android.R.styleable#Window_navigationBarDividerColor
+ * @deprecated This is no longer needed since the setter is deprecated.
*/
+ @Deprecated
public @ColorInt int getNavigationBarDividerColor() {
return 0;
}
@@ -2682,7 +2733,9 @@
* @see android.R.attr#enforceStatusBarContrast
* @see #isStatusBarContrastEnforced
* @see #setStatusBarColor
+ * @deprecated Draw proper background behind {@link WindowInsets.Type#statusBars()}} instead.
*/
+ @Deprecated
public void setStatusBarContrastEnforced(boolean ensureContrast) {
}
@@ -2697,7 +2750,9 @@
* @see android.R.attr#enforceStatusBarContrast
* @see #setStatusBarContrastEnforced
* @see #setStatusBarColor
+ * @deprecated This is not needed since the setter is deprecated.
*/
+ @Deprecated
public boolean isStatusBarContrastEnforced() {
return false;
}
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index c49fce5..848261d 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -179,9 +179,29 @@
}
}
+ /**
+ * Sets {@link com.android.server.wm.WindowManagerService} for the system process.
+ * <p>
+ * It is needed to prevent possible deadlock. A possible scenario is:
+ * In system process, WMS holds {@link com.android.server.wm.WindowManagerGlobalLock} to call
+ * {@code WindowManagerGlobal} APIs and wait to lock {@code WindowManagerGlobal} itself
+ * (i.e. call {@link #getWindowManagerService()} in the global lock), while
+ * another component may lock {@code WindowManagerGlobal} and wait to lock
+ * {@link com.android.server.wm.WindowManagerGlobalLock}(i.e call {@link #addView} in the
+ * system process, which calls to {@link com.android.server.wm.WindowManagerService} API
+ * directly).
+ */
+ public static void setWindowManagerServiceForSystemProcess(@NonNull IWindowManager wms) {
+ sWindowManagerService = wms;
+ }
+
@Nullable
@UnsupportedAppUsage
public static IWindowManager getWindowManagerService() {
+ if (sWindowManagerService != null) {
+ // Use WMS directly without locking WMGlobal to prevent deadlock.
+ return sWindowManagerService;
+ }
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
sWindowManagerService = IWindowManager.Stub.asInterface(
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 559ccfea7..7ebabee 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -64,6 +64,7 @@
import android.service.autofill.FillEventHistory;
import android.service.autofill.Flags;
import android.service.autofill.UserData;
+import android.service.credentials.CredentialProviderService;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -2382,7 +2383,18 @@
return;
}
- final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
+ final Parcelable result;
+ if (data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT) != null) {
+ result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
+ } else if (data.getParcelableExtra(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE) != null
+ && Flags.autofillCredmanIntegration()) {
+ result = data.getParcelableExtra(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE);
+ } else {
+ result = null;
+ }
+
final Bundle responseData = new Bundle();
responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result);
final Bundle newClientState = data.getBundleExtra(EXTRA_CLIENT_STATE);
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index 1dd99ba..6a4408b 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -1,13 +1,6 @@
package: "android.view.flags"
flag {
- name: "enable_surface_native_alloc_registration"
- namespace: "toolkit"
- description: "Feature flag for registering surfaces with the VM for faster cleanup"
- bug: "306193257"
-}
-
-flag {
name: "enable_surface_native_alloc_registration_ro"
namespace: "toolkit"
description: "Feature flag for registering surfaces with the VM for faster"
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index b4f9ee3..5239245 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -120,7 +120,6 @@
import android.window.ProxyOnBackInvokedDispatcher;
import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.view.menu.ContextMenuBuilder;
import com.android.internal.view.menu.IconMenuPresenter;
import com.android.internal.view.menu.ListMenuPresenter;
@@ -369,8 +368,7 @@
boolean mDecorFitsSystemWindows = true;
- @VisibleForTesting
- public final boolean mEdgeToEdgeEnforced;
+ private boolean mEdgeToEdgeEnforced;
private final ProxyOnBackInvokedDispatcher mProxyOnBackInvokedDispatcher;
@@ -390,11 +388,6 @@
mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context);
mAllowFloatingWindowsFillScreen = context.getResources().getBoolean(
com.android.internal.R.bool.config_allowFloatingWindowsFillScreen);
- mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(context.getApplicationInfo(), true /* local */);
- if (mEdgeToEdgeEnforced) {
- getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
- mDecorFitsSystemWindows = false;
- }
}
/**
@@ -436,15 +429,18 @@
*
* @param info The application to query.
* @param local Whether this is called from the process of the given application.
+ * @param windowStyle The style of the window.
* @return {@code true} if edge-to-edge is enforced. Otherwise, {@code false}.
*/
- public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local) {
- return info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
- || (Flags.enforceEdgeToEdge() && (local
- // Calling this doesn't require a permission.
- ? CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)
- // Calling this requires permissions.
- : info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)));
+ public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local,
+ TypedArray windowStyle) {
+ return !windowStyle.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false)
+ && (info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
+ || (Flags.enforceEdgeToEdge() && (local
+ // Calling this doesn't require a permission.
+ ? CompatChanges.isChangeEnabled(ENFORCE_EDGE_TO_EDGE)
+ // Calling this requires permissions.
+ : info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE))));
}
@Override
@@ -2470,6 +2466,13 @@
System.out.println(s);
}
+ mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(
+ getContext().getApplicationInfo(), true /* local */, a);
+ if (mEdgeToEdgeEnforced) {
+ getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
+ mDecorFitsSystemWindows = false;
+ }
+
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index a8d0d37..889434f 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -168,12 +168,12 @@
}
public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr) {
+ @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@@ -432,8 +432,14 @@
final List<MessagingMessage> newHistoricMessagingMessages =
createMessages(newHistoricMessages, /* isHistoric= */true, usePrecomputedText);
+ // Add our new MessagingMessages to groups
+ List<List<MessagingMessage>> groups = new ArrayList<>();
+ List<Person> senders = new ArrayList<>();
+ // Lets first find the groups (populate `groups` and `senders`)
+ findGroups(newHistoricMessagingMessages, newMessagingMessages, user, groups, senders);
+
return new MessagingData(user, showSpinner, unreadCount,
- newHistoricMessagingMessages, newMessagingMessages);
+ newHistoricMessagingMessages, newMessagingMessages, groups, senders);
}
/**
@@ -509,21 +515,13 @@
setUser(messagingData.getUser());
setUnreadCount(messagingData.getUnreadCount());
- List<MessagingMessage> messages = messagingData.getNewMessagingMessages();
- List<MessagingMessage> historicMessages = messagingData.getHistoricMessagingMessages();
// Copy our groups, before they get clobbered
ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
- // Add our new MessagingMessages to groups
- List<List<MessagingMessage>> groups = new ArrayList<>();
- List<Person> senders = new ArrayList<>();
-
- // Lets first find the groups (populate `groups` and `senders`)
- findGroups(historicMessages, messages, groups, senders);
-
// Let's now create the views and reorder them accordingly
// side-effect: updates mGroups, mAddedGroups
- createGroupViews(groups, senders, messagingData.getShowSpinner());
+ createGroupViews(messagingData.getGroups(), messagingData.getSenders(),
+ messagingData.getShowSpinner());
// Let's first check which groups were removed altogether and remove them in one animation
removeGroups(oldGroups);
@@ -536,8 +534,8 @@
historicMessage.removeMessage(mToRecycle);
}
- mMessages = messages;
- mHistoricMessages = historicMessages;
+ mMessages = messagingData.getNewMessagingMessages();
+ mHistoricMessages = messagingData.getHistoricMessagingMessages();
updateHistoricMessageVisibility();
updateTitleAndNamesDisplay();
@@ -935,7 +933,7 @@
}
private void createGroupViews(List<List<MessagingMessage>> groups,
- List<Person> senders, boolean showSpinner) {
+ List<Person> senders, boolean showSpinner) {
mGroups.clear();
for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
List<MessagingMessage> group = groups.get(groupIndex);
@@ -983,9 +981,12 @@
}
}
+ /**
+ * Finds groups and senders from the given messaging messages and fills outGroups and outSenders
+ */
private void findGroups(List<MessagingMessage> historicMessages,
- List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
- List<Person> senders) {
+ List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> outGroups,
+ List<Person> outSenders) {
CharSequence currentSenderKey = null;
List<MessagingMessage> currentGroup = null;
int histSize = historicMessages.size();
@@ -1003,14 +1004,14 @@
isNewGroup |= !TextUtils.equals(key, currentSenderKey);
if (isNewGroup) {
currentGroup = new ArrayList<>();
- groups.add(currentGroup);
+ outGroups.add(currentGroup);
if (sender == null) {
- sender = mUser;
+ sender = user;
} else {
// Remove all formatting from the sender name
sender = sender.toBuilder().setName(Objects.toString(sender.getName())).build();
}
- senders.add(sender);
+ outSenders.add(sender);
currentSenderKey = key;
}
currentGroup.add(message);
diff --git a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
index 5cda3f2..3e065bf 100644
--- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
+++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
@@ -16,8 +16,8 @@
package com.android.internal.widget;
+import static android.app.Flags.evenlyDividedCallStyleActionLayout;
import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT;
-import static android.app.Notification.CallStyle.USE_NEW_ACTION_LAYOUT;
import static android.text.style.DynamicDrawableSpan.ALIGN_CENTER;
import android.annotation.NonNull;
@@ -166,7 +166,7 @@
}
private void setIconToGlue(@Nullable Drawable icon) {
- if (!USE_NEW_ACTION_LAYOUT) {
+ if (!evenlyDividedCallStyleActionLayout()) {
Log.e(TAG, "glueIcon: new action layout disabled; doing nothing");
return;
}
@@ -207,7 +207,7 @@
}
private void setLabelToGlue(@Nullable CharSequence label) {
- if (!USE_NEW_ACTION_LAYOUT) {
+ if (!evenlyDividedCallStyleActionLayout()) {
Log.e(TAG, "glueLabel: new action layout disabled; doing nothing");
return;
}
@@ -255,7 +255,7 @@
return;
}
- if (!USE_NEW_ACTION_LAYOUT) {
+ if (!evenlyDividedCallStyleActionLayout()) {
Log.e(TAG, "glueIconAndLabelIfNeeded: new action layout disabled; doing nothing");
return;
}
diff --git a/core/java/com/android/internal/widget/MessagingData.java b/core/java/com/android/internal/widget/MessagingData.java
index 85b0201..42de60e 100644
--- a/core/java/com/android/internal/widget/MessagingData.java
+++ b/core/java/com/android/internal/widget/MessagingData.java
@@ -28,24 +28,33 @@
private final boolean mShowSpinner;
private final List<MessagingMessage> mHistoricMessagingMessages;
private final List<MessagingMessage> mNewMessagingMessages;
+ private final List<List<MessagingMessage>> mGroups;
+ private final List<Person> mSenders;
private final int mUnreadCount;
MessagingData(Person user, boolean showSpinner,
List<MessagingMessage> historicMessagingMessages,
- List<MessagingMessage> newMessagingMessages) {
+ List<MessagingMessage> newMessagingMessages, List<List<MessagingMessage>> groups,
+ List<Person> senders) {
this(user, showSpinner, /* unreadCount= */0,
- historicMessagingMessages, newMessagingMessages);
+ historicMessagingMessages, newMessagingMessages,
+ groups,
+ senders);
}
MessagingData(Person user, boolean showSpinner,
int unreadCount,
List<MessagingMessage> historicMessagingMessages,
- List<MessagingMessage> newMessagingMessages) {
+ List<MessagingMessage> newMessagingMessages,
+ List<List<MessagingMessage>> groups,
+ List<Person> senders) {
mUser = user;
mShowSpinner = showSpinner;
mUnreadCount = unreadCount;
mHistoricMessagingMessages = historicMessagingMessages;
mNewMessagingMessages = newMessagingMessages;
+ mGroups = groups;
+ mSenders = senders;
}
public Person getUser() {
@@ -67,4 +76,12 @@
public int getUnreadCount() {
return mUnreadCount;
}
+
+ public List<Person> getSenders() {
+ return mSenders;
+ }
+
+ public List<List<MessagingMessage>> getGroups() {
+ return mGroups;
+ }
}
diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java
index b6d7503..d000596 100644
--- a/core/java/com/android/internal/widget/MessagingLayout.java
+++ b/core/java/com/android/internal/widget/MessagingLayout.java
@@ -189,9 +189,15 @@
/* isHistoric= */true, usePrecomputedText);
final List<MessagingMessage> newMessagingMessages =
createMessages(newMessages, /* isHistoric */false, usePrecomputedText);
+ // Let's first find our groups!
+ List<List<MessagingMessage>> groups = new ArrayList<>();
+ List<Person> senders = new ArrayList<>();
+
+ // Lets first find the groups
+ findGroups(historicMessagingMessages, newMessagingMessages, groups, senders);
return new MessagingData(user, showSpinner,
- historicMessagingMessages, newMessagingMessages);
+ historicMessagingMessages, newMessagingMessages, groups, senders);
}
/**
@@ -256,10 +262,10 @@
private void bind(MessagingData messagingData) {
setUser(messagingData.getUser());
- List<MessagingMessage> historicMessages = messagingData.getHistoricMessagingMessages();
- List<MessagingMessage> messages = messagingData.getNewMessagingMessages();
+ // Let's now create the views and reorder them accordingly
ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
- addMessagesToGroups(historicMessages, messages, messagingData.getShowSpinner());
+ createGroupViews(messagingData.getGroups(), messagingData.getSenders(),
+ messagingData.getShowSpinner());
// Let's first check which groups were removed altogether and remove them in one animation
removeGroups(oldGroups);
@@ -272,8 +278,8 @@
historicMessage.removeMessage(mToRecycle);
}
- mMessages = messages;
- mHistoricMessages = historicMessages;
+ mMessages = messagingData.getNewMessagingMessages();
+ mHistoricMessages = messagingData.getHistoricMessagingMessages();
updateHistoricMessageVisibility();
updateTitleAndNamesDisplay();
@@ -451,19 +457,6 @@
}
}
- private void addMessagesToGroups(List<MessagingMessage> historicMessages,
- List<MessagingMessage> messages, boolean showSpinner) {
- // Let's first find our groups!
- List<List<MessagingMessage>> groups = new ArrayList<>();
- List<Person> senders = new ArrayList<>();
-
- // Lets first find the groups
- findGroups(historicMessages, messages, groups, senders);
-
- // Let's now create the views and reorder them accordingly
- createGroupViews(groups, senders, showSpinner);
- }
-
private void createGroupViews(List<List<MessagingMessage>> groups,
List<Person> senders, boolean showSpinner) {
mGroups.clear();
diff --git a/core/java/com/android/internal/widget/NotificationActionListLayout.java b/core/java/com/android/internal/widget/NotificationActionListLayout.java
index 69d2544..301dc39 100644
--- a/core/java/com/android/internal/widget/NotificationActionListLayout.java
+++ b/core/java/com/android/internal/widget/NotificationActionListLayout.java
@@ -17,7 +17,7 @@
package com.android.internal.widget;
import static android.app.Notification.CallStyle.DEBUG_NEW_ACTION_LAYOUT;
-import static android.app.Notification.CallStyle.USE_NEW_ACTION_LAYOUT;
+import static android.app.Flags.evenlyDividedCallStyleActionLayout;
import android.annotation.DimenRes;
import android.app.Notification;
@@ -410,7 +410,7 @@
*/
@RemotableViewMethod
public void setEvenlyDividedMode(boolean evenlyDividedMode) {
- if (evenlyDividedMode && !USE_NEW_ACTION_LAYOUT) {
+ if (evenlyDividedMode && !evenlyDividedCallStyleActionLayout()) {
Log.e(TAG, "setEvenlyDividedMode(true) called with new action layout disabled; "
+ "leaving evenly divided mode disabled");
return;
diff --git a/core/proto/OWNERS b/core/proto/OWNERS
index 381580b..c65794e 100644
--- a/core/proto/OWNERS
+++ b/core/proto/OWNERS
@@ -19,6 +19,7 @@
per-file android/hardware/sensorprivacy.proto = ntmyren@google.com,evanseverson@google.com
per-file background_install_control.proto = wenhaowang@google.com,georgechan@google.com,billylau@google.com
per-file android/content/intent.proto = file:/PACKAGE_MANAGER_OWNERS
+per-file android/hardware/location/context_hub_info.proto = file:/services/core/java/com/android/server/location/contexthub/OWNERS
# Biometrics
jaggies@google.com
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c71a842..5c764e2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3216,6 +3216,13 @@
android:description="@string/permdesc_accessHiddenProfile"
android:protectionLevel="normal" />
+ <!-- @SystemApi @hide Allows privileged applications to get details about hidden profile
+ users.
+ @FlaggedApi("android.multiuser.flags.enable_permission_to_access_hidden_profiles") -->
+ <permission
+ android:name="android.permission.ACCESS_HIDDEN_PROFILES_FULL"
+ android:protectionLevel="signature|privileged" />
+
<!-- @SystemApi @hide Allows starting activities across profiles in the same profile group. -->
<permission android:name="android.permission.START_CROSS_PROFILE_ACTIVITIES"
android:protectionLevel="signature|role" />
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index e7b1d09..908eeeb 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2292,7 +2292,17 @@
{@link android.R.attr#windowDrawsSystemBarBackgrounds} and the status bar must not
have been requested to be translucent with
{@link android.R.attr#windowTranslucentStatus}.
- Corresponds to {@link android.view.Window#setStatusBarColor(int)}. -->
+ Corresponds to {@link android.view.Window#setStatusBarColor(int)}.
+ <p>If the color is transparent and the window enforces the status bar contrast, the
+ system will determine whether a scrim is necessary and draw one on behalf of the app to
+ ensure that the status bar has enough contrast with the contents of this app, and set
+ an appropriate effective bar background accordingly.
+ See: {@link android.R.attr#enforceStatusBarContrast}
+ <p>If the app targets
+ {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ this attribute is ignored.
+ @deprecated Draw proper background behind
+ {@link android.view.WindowInsets.Type#statusBars()}} instead. -->
<attr name="statusBarColor" format="color" />
<!-- The color for the navigation bar. If the color is not opaque, consider setting
@@ -2302,7 +2312,18 @@
{@link android.R.attr#windowDrawsSystemBarBackgrounds} and the navigation bar must not
have been requested to be translucent with
{@link android.R.attr#windowTranslucentNavigation}.
- Corresponds to {@link android.view.Window#setNavigationBarColor(int)}. -->
+ Corresponds to {@link android.view.Window#setNavigationBarColor(int)}.
+ <p>If the color is transparent and the window enforces the navigation bar contrast, the
+ system will determine whether a scrim is necessary and draw one on behalf of the app to
+ ensure that the navigation bar has enough contrast with the contents of this app, and
+ set an appropriate effective bar background accordingly.
+ See: {@link android.R.attr#enforceNavigationBarContrast}
+ <p>If the app targets
+ {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ this attribute is ignored.
+ @deprecated Draw proper background behind
+ {@link android.view.WindowInsets.Type#navigationBars()} or
+ {@link android.view.WindowInsets.Type#tappableElement()} instead. -->
<attr name="navigationBarColor" format="color" />
<!-- Shows a thin line of the specified color between the navigation bar and the app
@@ -2311,7 +2332,13 @@
{@link android.R.attr#windowDrawsSystemBarBackgrounds} and the navigation bar must not
have been requested to be translucent with
{@link android.R.attr#windowTranslucentNavigation}.
- Corresponds to {@link android.view.Window#setNavigationBarDividerColor(int)}. -->
+ Corresponds to {@link android.view.Window#setNavigationBarDividerColor(int)}.
+ <p>If the app targets
+ {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above,
+ this attribute is ignored.
+ @deprecated Draw proper background behind
+ {@link android.view.WindowInsets.Type#navigationBars()} or
+ {@link android.view.WindowInsets.Type#tappableElement()} instead. -->
<attr name="navigationBarDividerColor" format="color" />
<!-- Sets whether the system should ensure that the status bar has enough
@@ -2327,7 +2354,9 @@
<p>If the app does not target at least {@link android.os.Build.VERSION_CODES#Q Q},
this attribute is ignored.
- @see android.view.Window#setStatusBarContrastEnforced -->
+ @see android.view.Window#setStatusBarContrastEnforced
+ @deprecated Draw proper background behind
+ {@link android.view.WindowInsets.Type#statusBars()}} instead. -->
<attr name="enforceStatusBarContrast" format="boolean" />
<!-- Sets whether the system should ensure that the navigation bar has enough
@@ -2483,6 +2512,31 @@
<!-- The icon is shown unless the launching app specified SPLASH_SCREEN_STYLE_EMPTY -->
<enum name="icon_preferred" value="1" />
</attr>
+
+ <!-- Flag indicating whether this window would opt-out the edge-to-edge enforcement.
+
+ <p>If this is false, the edge-to-edge enforcement will be applied to the window if its
+ app targets
+ {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM VANILLA_ICE_CREAM} or above.
+ The affected behaviors are:
+ <ul>
+ <li>The framework will not fit the content view to the insets and will just pass
+ through the {@link android.view.WindowInsets} to the content view, as if calling
+ {@link android.view.Window#setDecorFitsSystemWindows(boolean)} with false.
+ <li>{@link android.view.WindowManager.LayoutParams#layoutInDisplayCutoutMode} of
+ the non-floating windows will be set to {@link
+ android.view.WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS}.
+ Changing it to other values will cause {@link lang.IllegalArgumentException}.
+ <li>The framework will set {@link android.R.attr#statusBarColor},
+ {@link android.R.attr#navigationBarColor}, and
+ {@link android.R.attr#navigationBarDividerColor} to transparent.
+ </ul>
+
+ <p>If this is true, the edge-to-edge enforcement won't be applied. However, this
+ attribute will be deprecated and disabled in a future SDK level.
+
+ <p>This is false by default. -->
+ <attr name="windowOptOutEdgeToEdgeEnforcement" format="boolean"/>
</declare-styleable>
<!-- The set of attributes that describe a AlertDialog's theme. -->
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 81a8908..4799c37 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -147,6 +147,8 @@
<public name="useBoundsForWidth"/>
<!-- @FlaggedApi("android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP") -->
<public name="autoTransact"/>
+ <!-- @FlaggedApi("com.android.window.flags.enforce_edge_to_edge") -->
+ <public name="windowOptOutEdgeToEdgeEnforcement"/>
</staging-public-group>
<staging-public-group type="id" first-id="0x01bc0000">
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 9bb2499..61e6a36 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -127,7 +127,7 @@
<!-- France: 5 digits, free: 3xxxx, premium [4-8]xxxx, plus EU:
http://clients.txtnation.com/entries/161972-france-premium-sms-short-code-requirements,
visual voicemail code for Orange: 21101 -->
- <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051" />
+ <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051|33033" />
<!-- United Kingdom (Great Britain): 4-6 digits, common codes [5-8]xxxx, plus EU:
http://www.short-codes.com/media/Co-regulatoryCodeofPracticeforcommonshortcodes170206.pdf,
@@ -150,6 +150,9 @@
http://clients.txtnation.com/entries/209633-hungary-premium-sms-short-code-regulations -->
<shortcode country="hu" pattern="[01](?:\\d{3}|\\d{9})" premium="0691227910|1784" free="116\\d{3}" />
+ <!-- Honduras -->
+ <shortcode country="hn" pattern="\\d{4,6}" free="466453" />
+
<!-- India: 1-5 digits (standard system default, not country specific) -->
<shortcode country="in" pattern="\\d{1,5}" free="59336|53969" />
@@ -171,7 +174,7 @@
<shortcode country="jp" pattern="\\d{1,5}" free="8083" />
<!-- Kenya: 5 digits, known premium codes listed -->
- <shortcode country="ke" pattern="\\d{5}" free="21725|21562|40520|23342|40023" />
+ <shortcode country="ke" pattern="\\d{5}" free="21725|21562|40520|23342|40023|24088|23054" />
<!-- Kyrgyzstan: 4 digits, known premium codes listed -->
<shortcode country="kg" pattern="\\d{4}" premium="415[2367]|444[69]" />
@@ -183,7 +186,7 @@
<shortcode country="kz" pattern="\\d{4}" premium="335[02]|4161|444[469]|77[2359]0|8444|919[3-5]|968[2-5]" />
<!-- Kuwait: 1-5 digits (standard system default, not country specific) -->
- <shortcode country="kw" pattern="\\d{1,5}" free="1378|50420|94006|55991" />
+ <shortcode country="kw" pattern="\\d{1,5}" free="1378|50420|94006|55991|50976" />
<!-- Lithuania: 3-5 digits, known premium codes listed, plus EU -->
<shortcode country="lt" pattern="\\d{3,5}" premium="13[89]1|1394|16[34]5" free="116\\d{3}|1399|1324" />
@@ -195,9 +198,18 @@
<!-- Latvia: 4 digits, known premium codes listed, plus EU -->
<shortcode country="lv" pattern="\\d{4}" premium="18(?:19|63|7[1-4])" free="116\\d{3}|1399" />
+ <!-- Morocco: 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="ma" pattern="\\d{1,5}" free="53819" />
+
<!-- Macedonia: 1-6 digits (not confirmed), known premium codes listed -->
<shortcode country="mk" pattern="\\d{1,6}" free="129005|122" />
+ <!-- Malawi: 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="mw" pattern="\\d{1,5}" free="4276" />
+
+ <!-- Mozambique: 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="mz" pattern="\\d{1,5}" free="1714" />
+
<!-- Mexico: 4-5 digits (not confirmed), known premium codes listed -->
<shortcode country="mx" pattern="\\d{4,6}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963|91101|45453|550346" />
@@ -207,6 +219,9 @@
<!-- Namibia: 1-5 digits (standard system default, not country specific) -->
<shortcode country="na" pattern="\\d{1,5}" free="40005" />
+ <!-- Nicaragua -->
+ <shortcode country="ni" pattern="\\d{4,6}" free="466453" />
+
<!-- The Netherlands, 4 digits, known premium codes listed, plus EU -->
<shortcode country="nl" pattern="\\d{4}" premium="4466|5040" free="116\\d{3}|2223|6225|2223|1662" />
@@ -219,8 +234,8 @@
<!-- New Zealand: 3-4 digits, known premium codes listed -->
<shortcode country="nz" pattern="\\d{3,4}" premium="3903|8995|4679" free="1737|176|2141|3067|3068|3110|3876|4006|4053|4061|4062|4202|4300|4334|4412|4575|5626|8006|8681" />
- <!-- Peru: 4-5 digits (not confirmed), known premium codes listed -->
- <shortcode country="pe" pattern="\\d{4,5}" free="9963|40778" />
+ <!-- Peru: 4-6 digits (not confirmed), known premium codes listed -->
+ <shortcode country="pe" pattern="\\d{4,6}" free="9963|40778|301303" />
<!-- Philippines -->
<shortcode country="ph" pattern="\\d{1,5}" free="2147|5495|5496" />
@@ -269,6 +284,12 @@
<!-- Slovakia: 4 digits (premium), plus EU: http://www.cmtelecom.com/premium-sms/slovakia -->
<shortcode country="sk" premium="\\d{4}" free="116\\d{3}|8000" />
+ <!-- Senegal(SN): 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="sn" pattern="\\d{1,5}" free="21215" />
+
+ <!-- El Salvador(SV): 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="sv" pattern="\\d{4,6}" free="466453" />
+
<!-- Taiwan -->
<shortcode country="tw" pattern="\\d{4}" free="1922" />
@@ -278,15 +299,21 @@
<!-- Tajikistan: 4 digits, known premium codes listed -->
<shortcode country="tj" pattern="\\d{4}" premium="11[3-7]1|4161|4333|444[689]" />
+ <!-- Tanzania: 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="tz" pattern="\\d{1,5}" free="15046|15234" />
+
<!-- Turkey -->
<shortcode country="tr" pattern="\\d{1,5}" free="7529|5528|6493|3193" />
<!-- Ukraine: 4 digits, known premium codes listed -->
<shortcode country="ua" pattern="\\d{4}" premium="444[3-9]|70[579]4|7540" />
+ <!-- Uganda(UG): 4 digits (standard system default, not country specific) -->
+ <shortcode country="ug" pattern="\\d{4}" free="8000" />
+
<!-- USA: 5-6 digits (premium codes from https://www.premiumsmsrefunds.com/ShortCodes.htm),
visual voicemail code for T-Mobile: 122 -->
- <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611" />
+ <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831" />
<!-- Vietnam: 1-5 digits (standard system default, not country specific) -->
<shortcode country="vn" pattern="\\d{1,5}" free="5001|9055" />
diff --git a/core/tests/coretests/src/android/app/QueuedWorkTest.java b/core/tests/coretests/src/android/app/QueuedWorkTest.java
new file mode 100644
index 0000000..7021187
--- /dev/null
+++ b/core/tests/coretests/src/android/app/QueuedWorkTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.Semaphore;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+@SmallTest
+public class QueuedWorkTest {
+
+ private QueuedWork mQueuedWork;
+ private AtomicInteger mCounter;
+
+ private class AddToCounter implements Runnable {
+ private final int mDelta;
+
+ public AddToCounter(int delta) {
+ mDelta = delta;
+ }
+
+ @Override
+ public void run() {
+ mCounter.addAndGet(mDelta);
+ }
+ }
+
+ private class IncrementCounter extends AddToCounter {
+ public IncrementCounter() {
+ super(1);
+ }
+ }
+
+ @Before
+ public void setup() {
+ mQueuedWork = new QueuedWork();
+ mCounter = new AtomicInteger(0);
+ }
+
+ @After
+ public void teardown() {
+ mQueuedWork.waitToFinish();
+ QueuedWork.resetHandler();
+ }
+
+ @Test
+ public void testQueueThenWait() {
+ mQueuedWork.queue(new IncrementCounter(), false);
+ mQueuedWork.waitToFinish();
+ assertThat(mCounter.get()).isEqualTo(1);
+ assertThat(mQueuedWork.hasPendingWork()).isFalse();
+ }
+
+ @Test
+ public void testQueueWithDelayThenWait() {
+ mQueuedWork.queue(new IncrementCounter(), true);
+ mQueuedWork.waitToFinish();
+ assertThat(mCounter.get()).isEqualTo(1);
+ assertThat(mQueuedWork.hasPendingWork()).isFalse();
+ }
+
+ @Test
+ public void testWorkHappensNotOnCallerThread() {
+ AtomicBoolean childThreadStarted = new AtomicBoolean(false);
+ InheritableThreadLocal<Boolean> setTrueInChild =
+ new InheritableThreadLocal<Boolean>() {
+ @Override
+ protected Boolean initialValue() {
+ return false;
+ }
+
+ @Override
+ protected Boolean childValue(Boolean parentValue) {
+ childThreadStarted.set(true);
+ return true;
+ }
+ };
+
+ // Enqueue work to force a worker thread to be created
+ setTrueInChild.get();
+ assertThat(childThreadStarted.get()).isFalse();
+ mQueuedWork.queue(() -> setTrueInChild.get(), false);
+ mQueuedWork.waitToFinish();
+ assertThat(childThreadStarted.get()).isTrue();
+ }
+
+ @Test
+ public void testWaitToFinishDoesNotCreateThread() {
+ InheritableThreadLocal<Boolean> throwInChild =
+ new InheritableThreadLocal<Boolean>() {
+ @Override
+ protected Boolean initialValue() {
+ return false;
+ }
+
+ @Override
+ protected Boolean childValue(Boolean parentValue) {
+ throw new RuntimeException("New thread should not be started!");
+ }
+ };
+
+ try {
+ throwInChild.get();
+ // Intentionally don't enqueue work.
+ mQueuedWork.waitToFinish();
+ throwInChild.get();
+ // If a worker thread was unnecessarily started, we will have crashed.
+ } finally {
+ throwInChild.remove();
+ }
+ }
+
+ @Test
+ public void testFinisher() {
+ mQueuedWork.addFinisher(new AddToCounter(3));
+ mQueuedWork.addFinisher(new AddToCounter(7));
+ mQueuedWork.queue(new IncrementCounter(), false);
+ mQueuedWork.waitToFinish();
+ // The queued task and the two finishers all ran
+ assertThat(mCounter.get()).isEqualTo(1 + 3 + 7);
+ }
+
+ @Test
+ public void testRemoveFinisher() {
+ Runnable addThree = new AddToCounter(3);
+ Runnable addSeven = new AddToCounter(7);
+ mQueuedWork.addFinisher(addThree);
+ mQueuedWork.addFinisher(addSeven);
+ mQueuedWork.removeFinisher(addThree);
+ mQueuedWork.queue(new IncrementCounter(), false);
+ mQueuedWork.waitToFinish();
+ // The queued task and the two finishers all ran
+ assertThat(mCounter.get()).isEqualTo(1 + 7);
+ }
+
+ @Test
+ public void testHasPendingWork() {
+ Semaphore releaser = new Semaphore(0);
+ mQueuedWork.queue(
+ () -> {
+ try {
+ releaser.acquire();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }, false);
+ assertThat(mQueuedWork.hasPendingWork()).isTrue();
+ releaser.release();
+ mQueuedWork.waitToFinish();
+ assertThat(mQueuedWork.hasPendingWork()).isFalse();
+ }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/text/TextLineJustificationTest.kt b/core/tests/coretests/src/android/text/TextLineJustificationTest.kt
index a525615..c18f35f 100644
--- a/core/tests/coretests/src/android/text/TextLineJustificationTest.kt
+++ b/core/tests/coretests/src/android/text/TextLineJustificationTest.kt
@@ -126,4 +126,4 @@
TextLine.recycle(tl)
}
-}
\ No newline at end of file
+}
diff --git a/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt b/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt
index 27869bb..71980c1 100644
--- a/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt
+++ b/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt
@@ -21,7 +21,7 @@
import android.platform.test.flag.junit.DeviceFlagsValueProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION
+import com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -40,7 +40,7 @@
@JvmField
val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
- @RequiresFlagsEnabled(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @RequiresFlagsEnabled(FLAG_LETTER_SPACING_JUSTIFICATION)
@Test
fun calculateRunFlagTest() {
// Only one Bidi run
@@ -84,7 +84,7 @@
.isEqualTo(LEFT_EDGE)
}
- @RequiresFlagsEnabled(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @RequiresFlagsEnabled(FLAG_LETTER_SPACING_JUSTIFICATION)
@Test
fun resolveRunFlagForSubSequenceTest() {
val runStart = 5
@@ -221,4 +221,4 @@
MIDDLE_OF_LINE, true, runStart, runEnd, runStart + 1, runEnd - 1))
.isEqualTo(MIDDLE_OF_LINE)
}
-}
\ No newline at end of file
+}
diff --git a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
index de55b07..3df3b9d2 100644
--- a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
+++ b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
@@ -20,6 +20,7 @@
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
@@ -63,7 +64,8 @@
createPhoneWindowWithTheme(R.style.LayoutInDisplayCutoutModeUnset);
installDecor();
- if (mPhoneWindow.mEdgeToEdgeEnforced && !mPhoneWindow.isFloating()) {
+ if ((mPhoneWindow.getAttributes().privateFlags & PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED) != 0
+ && !mPhoneWindow.isFloating()) {
assertThat(mPhoneWindow.getAttributes().layoutInDisplayCutoutMode,
is(LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS));
} else {
diff --git a/data/etc/enhanced-confirmation.xml b/data/etc/enhanced-confirmation.xml
index 4a9dd2f..3b1867c 100644
--- a/data/etc/enhanced-confirmation.xml
+++ b/data/etc/enhanced-confirmation.xml
@@ -21,12 +21,36 @@
Example usage:
- <enhanced-confirmation-trusted-installer
+ <enhanced-confirmation-trusted-package
package="com.example.app"
- signature="E9:7A:BC:2C:D1:CA:8D:58:6A:57:0B:8C:F8:60:AA:D2:8D:13:30:2A:FB:C9:00:2C:5D:53:B2:6C:09:A4:85:A0"/>
+ sha256-cert-digest="E9:7A:BC:2C:D1:CA:8D:58:6A:57:0B:8C:F8:60:AA:D2:8D:13:30:2A:FB:C9:00:2C:5D:53:B2:6C:09:A4:85:A0"/>
-This indicates that "com.example.app" should be exempt from ECM, and that, if "com.example.app" is
-an installer, all packages installed via "com.example.app" will also be exempt from ECM.
+ ...
+
+ <enhanced-confirmation-trusted-installer
+ package="com.example.installer"
+ sha256-cert-digest="E9:7A:BC:2C:D1:CA:8D:58:6A:57:0B:8C:F8:60:AA:D2:8D:13:30:2A:FB:C9:00:2C:5D:53:B2:6C:09:A4:85:A0"/>
+
+ ...
+
+The "enhanced-confirmation-trusted-package" entry shown above indicates that "com.example.app"
+should be considered a "trusted package". A "trusted package" will be exempt from ECM restrictions.
+
+The "enhanced-confirmation-trusted-installer" entry shown above indicates that
+"com.example.installer" should be considered a "trusted installer". A "trusted installer", and all
+packages that it installs, will be exempt from ECM restrictions. (There are some exceptions to this.
+For example, a trusted installer, at the time of installing an app, can opt the app back in to ECM
+restrictions by setting the app's package source to PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
+or PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE.)
+
+In either case:
+
+- The "package" XML attribute refers to the app's package name.
+- The "sha256-cert-digest" XML attribute refers to the SHA-256 hash of an app signing certificate.
+
+For any entry to successfully apply to a package, both XML attributes must be present, and must
+match the package. That is, the package name must match the "package" attribute, and the app must be
+signed by the signing certificate identified by the "sha256-cert-digest" attribute..
-->
<config></config>
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index ae61a2d..df95a91 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -17,7 +17,7 @@
package android.graphics;
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
-import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION;
+import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
import android.annotation.ColorInt;
import android.annotation.ColorLong;
@@ -293,7 +293,7 @@
* {@link Paint#measureText(CharSequence, int, int)}. The non-run based APIs works as both
* {@link #TEXT_RUN_FLAG_LEFT_EDGE} and {@link #TEXT_RUN_FLAG_RIGHT_EDGE} are specified.
*/
- @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
public static final int TEXT_RUN_FLAG_LEFT_EDGE = 0x2000;
@@ -324,7 +324,7 @@
* {@link Paint#measureText(CharSequence, int, int)}. The non-run based APIs works as both
* {@link #TEXT_RUN_FLAG_LEFT_EDGE} and {@link #TEXT_RUN_FLAG_RIGHT_EDGE} are specified.
*/
- @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 0x4000;
// These flags are always set on a new/reset paint, even if flags 0 is passed.
diff --git a/graphics/java/android/graphics/text/LineBreaker.java b/graphics/java/android/graphics/text/LineBreaker.java
index 9707126..0e9f29d 100644
--- a/graphics/java/android/graphics/text/LineBreaker.java
+++ b/graphics/java/android/graphics/text/LineBreaker.java
@@ -17,7 +17,7 @@
package android.graphics.text;
import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
-import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION;
+import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
@@ -183,7 +183,7 @@
/**
* Value for justification mode indicating the text is justified by stretching letter spacing.
*/
- @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION)
public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2;
/**
diff --git a/keystore/java/android/security/AndroidKeyStoreMaintenance.java b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
index 2beb434..2430e8d 100644
--- a/keystore/java/android/security/AndroidKeyStoreMaintenance.java
+++ b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.os.StrictMode;
@@ -218,4 +219,28 @@
return SYSTEM_ERROR;
}
}
+
+ /**
+ * Returns the list of Application UIDs that have auth-bound keys that are bound to
+ * the given SID. This enables warning the user when they are about to invalidate
+ * a SID (for example, removing the LSKF).
+ *
+ * @param userId - The ID of the user the SID is associated with.
+ * @param userSecureId - The SID in question.
+ *
+ * @return A list of app UIDs.
+ */
+ public static long[] getAllAppUidsAffectedBySid(int userId, long userSecureId)
+ throws KeyStoreException {
+ StrictMode.noteDiskWrite();
+ try {
+ return getService().getAppUidsAffectedBySid(userId, userSecureId);
+ } catch (RemoteException | NullPointerException e) {
+ throw new KeyStoreException(SYSTEM_ERROR,
+ "Failure to connect to Keystore while trying to get apps affected by SID.");
+ } catch (ServiceSpecificException e) {
+ throw new KeyStoreException(e.errorCode,
+ "Keystore error while trying to get apps affected by SID.");
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 9854e58..a80afe2 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -45,9 +45,6 @@
<!-- Allow PIP to resize to a slightly bigger state upon touch/showing the menu -->
<bool name="config_pipEnableResizeForMenu">true</bool>
- <!-- Allow PIP to resize via dragging the corner of PiP. -->
- <bool name="config_pipEnableDragCornerResize">false</bool>
-
<!-- PiP minimum size, which is a % based off the shorter side of display width and height -->
<fraction name="config_pipShortestEdgePercent">40%</fraction>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index a2a2914..7a4ad0a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -912,9 +912,8 @@
if (uid != -1) {
intent.putExtra(Settings.EXTRA_APP_UID, uid);
}
- intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
return intent;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
index 8b6c7b6..68d26da 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
@@ -140,7 +140,7 @@
// spec takes the aspect ratio of the bounds into account, so both width and height
// scale by the same factor.
addPipExclusionBoundsChangeCallback((bounds) -> {
- mBoundsScale = Math.min((float) bounds.width() / mMaxSize.x, 1.0f);
+ updateBoundsScale();
});
}
@@ -152,6 +152,11 @@
mSizeSpecSource.onConfigurationChanged();
}
+ /** Update the bounds scale percentage value. */
+ public void updateBoundsScale() {
+ mBoundsScale = Math.min((float) mBounds.width() / mMaxSize.x, 1.0f);
+ }
+
private void reloadResources() {
mStashOffset = mContext.getResources().getDimensionPixelSize(R.dimen.pip_stash_offset);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 896ca96..e018ecc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -286,12 +286,6 @@
// For transition that we don't animate, but contains the PIP leash, we need to update the
// PIP surface, otherwise it will be reset after the transition.
if (currentPipTaskChange != null) {
- // Set the "end" bounds of pip. The default setup uses the start bounds. Since this is
- // changing the *finish*Transaction, we need to use the end bounds. This will also
- // make sure that the fade-in animation (below) uses the end bounds as well.
- if (!currentPipTaskChange.getEndAbsBounds().isEmpty()) {
- mPipBoundsState.setBounds(currentPipTaskChange.getEndAbsBounds());
- }
updatePipForUnhandledTransition(currentPipTaskChange, startTransaction,
finishTransaction);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index c5a0102..05d4f53 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -244,6 +244,9 @@
// The same rotation may have been set by auto PiP-able or fixed rotation. So notify
// the change with fromRotation=false to apply the rotated destination bounds from
// PipTaskOrganizer#onMovementBoundsChanged.
+ // We need to update the bounds scale in case this was from fixed rotation, as the
+ // current proportion was computed using the previous orientation max size and is wrong.
+ mPipBoundsState.updateBoundsScale();
updateMovementBounds(null, false /* fromRotation */,
false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t);
return;
@@ -797,21 +800,15 @@
mPipBoundsAlgorithm.getMovementBounds(postChangeBounds),
mPipBoundsState.getStashedState());
- // Scale PiP on density dpi change, so it appears to be the same size physically.
- final boolean densityDpiChanged =
- mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0
- && (mPipDisplayLayoutState.getDisplayLayout().densityDpi()
- != layout.densityDpi());
- if (densityDpiChanged) {
- final float scale = (float) layout.densityDpi()
- / mPipDisplayLayoutState.getDisplayLayout().densityDpi();
- postChangeBounds.set(0, 0,
- (int) (postChangeBounds.width() * scale),
- (int) (postChangeBounds.height() * scale));
- }
-
updateDisplayLayout.run();
+ // Resize the PiP bounds to be at the same scale relative to the new size spec. For
+ // example, if PiP was resized to 90% of the maximum size on the previous layout,
+ // make sure it is 90% of the new maximum size spec.
+ postChangeBounds.set(0, 0,
+ (int) (mPipBoundsState.getMaxSize().x * mPipBoundsState.getBoundsScale()),
+ (int) (mPipBoundsState.getMaxSize().y * mPipBoundsState.getBoundsScale()));
+
// Calculate the PiP bounds in the new orientation based on same fraction along the
// rotated movement bounds.
final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds(
@@ -827,6 +824,10 @@
mPipBoundsState.setHasUserResizedPip(true);
mTouchHandler.setUserResizeBounds(postChangeBounds);
+ final boolean densityDpiChanged =
+ mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0
+ && (mPipDisplayLayoutState.getDisplayLayout().densityDpi()
+ != layout.densityDpi());
if (densityDpiChanged) {
// Using PipMotionHelper#movePip directly here may cause race condition since
// the app content in PiP mode may or may not be updated for the new density dpi.
@@ -1146,6 +1147,11 @@
// Update the display layout
mPipDisplayLayoutState.rotateTo(toRotation);
+ mTouchHandler.updateMinMaxSize(mPipBoundsState.getAspectRatio());
+
+ postChangeStackBounds.set(0, 0,
+ (int) (mPipBoundsState.getMaxSize().x * mPipBoundsState.getBoundsScale()),
+ (int) (mPipBoundsState.getMaxSize().y * mPipBoundsState.getBoundsScale()));
// Calculate the stack bounds in the new orientation based on same fraction along the
// rotated movement bounds.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
index f175775..5f9195a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
@@ -15,19 +15,13 @@
*/
package com.android.wm.shell.pip.phone;
-import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM;
-import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT;
import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE;
-import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT;
-import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP;
-import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
-import android.graphics.Region;
import android.hardware.input.InputManager;
import android.os.Looper;
import android.view.BatchedInputEventReceiver;
@@ -41,7 +35,6 @@
import androidx.annotation.VisibleForTesting;
-import com.android.internal.policy.TaskResizingAlgorithm;
import com.android.wm.shell.R;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
@@ -53,7 +46,6 @@
import java.io.PrintWriter;
import java.util.function.Consumer;
-import java.util.function.Function;
/**
* Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to
@@ -77,7 +69,6 @@
private final PipPinchResizingAlgorithm mPinchResizingAlgorithm;
private final int mDisplayId;
private final ShellExecutor mMainExecutor;
- private final Region mTmpRegion = new Region();
private final PointF mDownPoint = new PointF();
private final PointF mDownSecondPoint = new PointF();
@@ -88,24 +79,15 @@
private final Rect mLastResizeBounds = new Rect();
private final Rect mUserResizeBounds = new Rect();
private final Rect mDownBounds = new Rect();
- private final Rect mDragCornerSize = new Rect();
- private final Rect mTmpTopLeftCorner = new Rect();
- private final Rect mTmpTopRightCorner = new Rect();
- private final Rect mTmpBottomLeftCorner = new Rect();
- private final Rect mTmpBottomRightCorner = new Rect();
- private final Rect mDisplayBounds = new Rect();
- private final Function<Rect, Rect> mMovementBoundsSupplier;
private final Runnable mUpdateMovementBoundsRunnable;
private final Consumer<Rect> mUpdateResizeBoundsCallback;
- private int mDelta;
private float mTouchSlop;
private boolean mAllowGesture;
private boolean mIsAttached;
private boolean mIsEnabled;
private boolean mEnablePinchResize;
- private boolean mEnableDragCornerResize;
private boolean mIsSysUiStateValid;
private boolean mThresholdCrossed;
private boolean mOngoingPinchToResize = false;
@@ -123,7 +105,7 @@
PipBoundsState pipBoundsState, PipMotionHelper motionHelper,
PipTouchState pipTouchState, PipTaskOrganizer pipTaskOrganizer,
PipDismissTargetHandler pipDismissTargetHandler,
- Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable,
+ Runnable updateMovementBoundsRunnable,
PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController,
ShellExecutor mainExecutor) {
mContext = context;
@@ -135,7 +117,6 @@
mPipTouchState = pipTouchState;
mPipTaskOrganizer = pipTaskOrganizer;
mPipDismissTargetHandler = pipDismissTargetHandler;
- mMovementBoundsSupplier = movementBoundsSupplier;
mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable;
mPhonePipMenuController = menuActivityController;
mPipUiEventLogger = pipUiEventLogger;
@@ -171,20 +152,9 @@
}
private void reloadResources() {
- final Resources res = mContext.getResources();
- mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size);
- mEnableDragCornerResize = res.getBoolean(R.bool.config_pipEnableDragCornerResize);
mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
}
- private void resetDragCorners() {
- mDragCornerSize.set(0, 0, mDelta, mDelta);
- mTmpTopLeftCorner.set(mDragCornerSize);
- mTmpTopRightCorner.set(mDragCornerSize);
- mTmpBottomLeftCorner.set(mDragCornerSize);
- mTmpBottomRightCorner.set(mDragCornerSize);
- }
-
private void disposeInputChannel() {
if (mInputEventReceiver != null) {
mInputEventReceiver.dispose();
@@ -232,7 +202,7 @@
@VisibleForTesting
void onInputEvent(InputEvent ev) {
- if (!mEnableDragCornerResize && !mEnablePinchResize) {
+ if (!mEnablePinchResize) {
// No need to handle anything if neither form of resizing is enabled.
return;
}
@@ -260,8 +230,6 @@
if (mEnablePinchResize && mOngoingPinchToResize) {
onPinchResize(mv);
- } else if (mEnableDragCornerResize) {
- onDragCornerResize(mv);
}
}
}
@@ -273,48 +241,6 @@
return mCtrlType != CTRL_NONE || mOngoingPinchToResize;
}
- /**
- * Check whether the current x,y coordinate is within the region in which drag-resize should
- * start.
- * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which
- * overlaps with the PIP window while the rest goes outside of the PIP window.
- * _ _ _ _
- * |_|_|_________|_|_|
- * |_|_| |_|_|
- * | PIP |
- * | WINDOW |
- * _|_ _|_
- * |_|_|_________|_|_|
- * |_|_| |_|_|
- */
- public boolean isWithinDragResizeRegion(int x, int y) {
- if (!mEnableDragCornerResize) {
- return false;
- }
-
- final Rect currentPipBounds = mPipBoundsState.getBounds();
- if (currentPipBounds == null) {
- return false;
- }
- resetDragCorners();
- mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2,
- currentPipBounds.top - mDelta / 2);
- mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2,
- currentPipBounds.top - mDelta / 2);
- mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2,
- currentPipBounds.bottom - mDelta / 2);
- mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2,
- currentPipBounds.bottom - mDelta / 2);
-
- mTmpRegion.setEmpty();
- mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION);
- mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION);
- mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION);
- mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION);
-
- return mTmpRegion.contains(x, y);
- }
-
public boolean isUsingPinchToZoom() {
return mEnablePinchResize;
}
@@ -325,62 +251,17 @@
public boolean willStartResizeGesture(MotionEvent ev) {
if (isInValidSysUiState()) {
- switch (ev.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- if (isWithinDragResizeRegion((int) ev.getRawX(), (int) ev.getRawY())) {
- return true;
- }
- break;
-
- case MotionEvent.ACTION_POINTER_DOWN:
- if (mEnablePinchResize && ev.getPointerCount() == 2) {
- onPinchResize(ev);
- mOngoingPinchToResize = mAllowGesture;
- return mAllowGesture;
- }
- break;
-
- default:
- break;
+ if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
+ if (mEnablePinchResize && ev.getPointerCount() == 2) {
+ onPinchResize(ev);
+ mOngoingPinchToResize = mAllowGesture;
+ return mAllowGesture;
+ }
}
}
return false;
}
- private void setCtrlType(int x, int y) {
- final Rect currentPipBounds = mPipBoundsState.getBounds();
-
- Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds);
-
- mDisplayBounds.set(movementBounds.left,
- movementBounds.top,
- movementBounds.right + currentPipBounds.width(),
- movementBounds.bottom + currentPipBounds.height());
-
- if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top
- && currentPipBounds.left != mDisplayBounds.left) {
- mCtrlType |= CTRL_LEFT;
- mCtrlType |= CTRL_TOP;
- }
- if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top
- && currentPipBounds.right != mDisplayBounds.right) {
- mCtrlType |= CTRL_RIGHT;
- mCtrlType |= CTRL_TOP;
- }
- if (mTmpBottomRightCorner.contains(x, y)
- && currentPipBounds.bottom != mDisplayBounds.bottom
- && currentPipBounds.right != mDisplayBounds.right) {
- mCtrlType |= CTRL_RIGHT;
- mCtrlType |= CTRL_BOTTOM;
- }
- if (mTmpBottomLeftCorner.contains(x, y)
- && currentPipBounds.bottom != mDisplayBounds.bottom
- && currentPipBounds.left != mDisplayBounds.left) {
- mCtrlType |= CTRL_LEFT;
- mCtrlType |= CTRL_BOTTOM;
- }
- }
-
private boolean isInValidSysUiState() {
return mIsSysUiStateValid;
}
@@ -457,59 +338,6 @@
}
}
- private void onDragCornerResize(MotionEvent ev) {
- int action = ev.getActionMasked();
- float x = ev.getX();
- float y = ev.getY() - mOhmOffset;
- if (action == MotionEvent.ACTION_DOWN) {
- mLastResizeBounds.setEmpty();
- mAllowGesture = isInValidSysUiState() && isWithinDragResizeRegion((int) x, (int) y);
- if (mAllowGesture) {
- setCtrlType((int) x, (int) y);
- mDownPoint.set(x, y);
- mDownBounds.set(mPipBoundsState.getBounds());
- }
- } else if (mAllowGesture) {
- switch (action) {
- case MotionEvent.ACTION_POINTER_DOWN:
- // We do not support multi touch for resizing via drag
- mAllowGesture = false;
- break;
- case MotionEvent.ACTION_MOVE:
- // Capture inputs
- if (!mThresholdCrossed
- && Math.hypot(x - mDownPoint.x, y - mDownPoint.y) > mTouchSlop) {
- mThresholdCrossed = true;
- // Reset the down to begin resizing from this point
- mDownPoint.set(x, y);
- mInputMonitor.pilferPointers();
- }
- if (mThresholdCrossed) {
- if (mPhonePipMenuController.isMenuVisible()) {
- mPhonePipMenuController.hideMenu(ANIM_TYPE_NONE,
- false /* resize */);
- }
- final Rect currentPipBounds = mPipBoundsState.getBounds();
- mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y,
- mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x,
- mMinSize.y, mMaxSize, true,
- mDownBounds.width() > mDownBounds.height()));
- mPipBoundsAlgorithm.transformBoundsToAspectRatio(mLastResizeBounds,
- mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */,
- true /* useCurrentSize */);
- mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds,
- null);
- mPipBoundsState.setHasUserResizedPip(true);
- }
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- finishResize();
- break;
- }
- }
- }
-
private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) {
final int leftEdge = bounds.left;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 81705e2..11c356d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -212,7 +212,7 @@
mPipResizeGestureHandler =
new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState,
mMotionHelper, mTouchState, pipTaskOrganizer, mPipDismissTargetHandler,
- this::getMovementBounds, this::updateMovementBounds, pipUiEventLogger,
+ this::updateMovementBounds, pipUiEventLogger,
menuController, mainExecutor);
mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState,
mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index a666e20..bfb60c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -253,7 +253,7 @@
: taskInfo.topActivityInfo;
params.layoutInDisplayCutoutMode = a.getInt(
R.styleable.Window_windowLayoutInDisplayCutoutMode,
- PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */)
+ PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */, a)
? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
: params.layoutInDisplayCutoutMode);
params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
index 8207b85..07cd682 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
@@ -86,6 +86,8 @@
.withNavOrTaskBarVisible()
.withStatusBarVisible()
.waitForAndVerify()
+
+ pipApp.tapPipToShowMenu(wmHelper)
}
}
@@ -194,6 +196,16 @@
}
}
+ @Postsubmit
+ @Test
+ fun menuOverlayMatchesTaskSurface() {
+ flicker.assertLayersEnd {
+ val pipAppRegion = visibleRegion(pipApp)
+ val pipMenuRegion = visibleRegion(ComponentNameMatcher.PIP_MENU_OVERLAY)
+ pipAppRegion.coversExactly(pipMenuRegion.region)
+ }
+ }
+
/** {@inheritDoc} */
@FlakyTest(bugId = 267424412)
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index 9719ba8..cc726cb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -121,7 +121,7 @@
mPipResizeGestureHandler = new PipResizeGestureHandler(mContext, pipBoundsAlgorithm,
mPipBoundsState, motionHelper, mPipTouchState, mPipTaskOrganizer,
mPipDismissTargetHandler,
- (Rect bounds) -> new Rect(), () -> {}, mPipUiEventLogger, mPhonePipMenuController,
+ () -> {}, mPipUiEventLogger, mPhonePipMenuController,
mMainExecutor) {
@Override
public void pilferPointers() {
diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h
index 6ebfc63..ac75c07 100644
--- a/libs/hwui/FeatureFlags.h
+++ b/libs/hwui/FeatureFlags.h
@@ -41,9 +41,9 @@
#endif // __ANDROID__
}
-inline bool inter_character_justification() {
+inline bool letter_spacing_justification() {
#ifdef __ANDROID__
- return com_android_text_flags_inter_character_justification();
+ return com_android_text_flags_letter_spacing_justification();
#else
return true;
#endif // __ANDROID__
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index 5613369..d66d7f8 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -72,7 +72,7 @@
const minikin::Range contextRange(contextStart, contextStart + contextCount);
const minikin::StartHyphenEdit startHyphen = paint->getStartHyphenEdit();
const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit();
- const minikin::RunFlag minikinRunFlag = text_feature::inter_character_justification()
+ const minikin::RunFlag minikinRunFlag = text_feature::letter_spacing_justification()
? paint->getRunFlag()
: minikin::RunFlag::NONE;
@@ -105,7 +105,7 @@
const minikin::Range range(start, start + count);
const minikin::StartHyphenEdit startHyphen = paint->getStartHyphenEdit();
const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit();
- const minikin::RunFlag minikinRunFlag = text_feature::inter_character_justification()
+ const minikin::RunFlag minikinRunFlag = text_feature::letter_spacing_justification()
? paint->getRunFlag()
: minikin::RunFlag::NONE;
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index 4fbe9ee..6ac9695 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -40,17 +40,6 @@
},
{
"file_patterns": [
- "[^/]*(Ringtone)[^/]*\\.java"
- ],
- "name": "MediaRingtoneTests",
- "options": [
- {"exclude-annotation": "androidx.test.filters.LargeTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"},
- {"exclude-annotation": "org.junit.Ignore"}
- ]
- },
- {
- "file_patterns": [
"[^/]*(LoudnessCodec)[^/]*\\.java"
],
"name": "LoudnessCodecApiTest",
diff --git a/media/java/android/media/IRingtonePlayer.aidl b/media/java/android/media/IRingtonePlayer.aidl
index 1e57be2..c96a400 100644
--- a/media/java/android/media/IRingtonePlayer.aidl
+++ b/media/java/android/media/IRingtonePlayer.aidl
@@ -21,7 +21,6 @@
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
-import android.os.VibrationEffect;
/**
* @hide
@@ -30,23 +29,12 @@
/** Used for Ringtone.java playback */
@UnsupportedAppUsage
oneway void play(IBinder token, in Uri uri, in AudioAttributes aa, float volume, boolean looping);
+ oneway void playWithVolumeShaping(IBinder token, in Uri uri, in AudioAttributes aa,
+ float volume, boolean looping, in @nullable VolumeShaper.Configuration volumeShaperConfig);
oneway void stop(IBinder token);
boolean isPlaying(IBinder token);
-
- // RingtoneV1
- oneway void playWithVolumeShaping(IBinder token, in Uri uri, in AudioAttributes aa,
- float volume, boolean looping, in @nullable VolumeShaper.Configuration volumeShaperConfig);
oneway void setPlaybackProperties(IBinder token, float volume, boolean looping,
- boolean hapticGeneratorEnabled);
-
- // RingtoneV2
- oneway void playRemoteRingtone(IBinder token, in Uri uri, in AudioAttributes aa,
- boolean useExactAudioAttributes, int enabledMedia, in @nullable VibrationEffect ve,
- float volume, boolean looping, boolean hapticGeneratorEnabled,
- in @nullable VolumeShaper.Configuration volumeShaperConfig);
- oneway void setLooping(IBinder token, boolean looping);
- oneway void setVolume(IBinder token, float volume);
- oneway void setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled);
+ boolean hapticGeneratorEnabled);
/** Used for Notification sound playback. */
oneway void playAsync(in Uri uri, in UserHandle user, boolean looping, in AudioAttributes aa, float volume);
diff --git a/media/java/android/media/LocalRingtonePlayer.java b/media/java/android/media/LocalRingtonePlayer.java
deleted file mode 100644
index fe7cc3e..0000000
--- a/media/java/android/media/LocalRingtonePlayer.java
+++ /dev/null
@@ -1,408 +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 android.media;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.res.AssetFileDescriptor;
-import android.media.audiofx.HapticGenerator;
-import android.net.Uri;
-import android.os.Trace;
-import android.os.VibrationAttributes;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.util.Log;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Objects;
-
-/**
- * Plays a ringtone on the local process.
- * @hide
- */
-public class LocalRingtonePlayer
- implements RingtoneV2.RingtonePlayer, MediaPlayer.OnCompletionListener {
- private static final String TAG = "LocalRingtonePlayer";
-
- // keep references on active Ringtones until stopped or completion listener called.
- private static final ArrayList<LocalRingtonePlayer> sActiveMediaPlayers = new ArrayList<>();
-
- private final MediaPlayer mMediaPlayer;
- private final AudioAttributes mAudioAttributes;
- private final RingtoneV2.RingtonePlayer mVibrationPlayer;
- private final Ringtone.Injectables mInjectables;
- private final AudioManager mAudioManager;
- private final VolumeShaper mVolumeShaper;
- private HapticGenerator mHapticGenerator;
-
- private LocalRingtonePlayer(@NonNull MediaPlayer mediaPlayer,
- @NonNull AudioAttributes audioAttributes, @NonNull Ringtone.Injectables injectables,
- @NonNull AudioManager audioManager, @Nullable HapticGenerator hapticGenerator,
- @Nullable VolumeShaper volumeShaper,
- @Nullable RingtoneV2.RingtonePlayer vibrationPlayer) {
- Objects.requireNonNull(mediaPlayer);
- Objects.requireNonNull(audioAttributes);
- Objects.requireNonNull(injectables);
- Objects.requireNonNull(audioManager);
- mMediaPlayer = mediaPlayer;
- mAudioAttributes = audioAttributes;
- mInjectables = injectables;
- mAudioManager = audioManager;
- mVolumeShaper = volumeShaper;
- mVibrationPlayer = vibrationPlayer;
- mHapticGenerator = hapticGenerator;
- }
-
- /**
- * Creates a {@link LocalRingtonePlayer} for a Uri, returning null if the Uri can't be
- * loaded in the local player.
- */
- @Nullable
- static RingtoneV2.RingtonePlayer create(@NonNull Context context,
- @NonNull AudioManager audioManager, @NonNull Vibrator vibrator,
- @NonNull Uri soundUri,
- @NonNull AudioAttributes audioAttributes,
- boolean isVibrationOnly,
- @Nullable VibrationEffect vibrationEffect,
- @NonNull Ringtone.Injectables injectables,
- @Nullable VolumeShaper.Configuration volumeShaperConfig,
- @Nullable AudioDeviceInfo preferredDevice, boolean initialHapticGeneratorEnabled,
- boolean initialLooping, float initialVolume) {
- Objects.requireNonNull(context);
- Objects.requireNonNull(soundUri);
- Objects.requireNonNull(audioAttributes);
- Trace.beginSection("createLocalMediaPlayer");
- MediaPlayer mediaPlayer = injectables.newMediaPlayer();
- HapticGenerator hapticGenerator = null;
- try {
- mediaPlayer.setDataSource(context, soundUri);
- mediaPlayer.setAudioAttributes(audioAttributes);
- mediaPlayer.setPreferredDevice(preferredDevice);
- mediaPlayer.setLooping(initialLooping);
- mediaPlayer.setVolume(isVibrationOnly ? 0 : initialVolume);
- if (initialHapticGeneratorEnabled) {
- hapticGenerator = injectables.createHapticGenerator(mediaPlayer);
- if (hapticGenerator != null) {
- // In practise, this should always be non-null because the initial value is
- // not true unless it's available.
- hapticGenerator.setEnabled(true);
- vibrationEffect = null; // Don't play the VibrationEffect.
- }
- }
- VolumeShaper volumeShaper = null;
- if (volumeShaperConfig != null) {
- volumeShaper = mediaPlayer.createVolumeShaper(volumeShaperConfig);
- }
- mediaPlayer.prepare();
- if (vibrationEffect != null && !audioAttributes.areHapticChannelsMuted()) {
- if (injectables.hasHapticChannels(mediaPlayer)) {
- // Don't play the Vibration effect if the URI has haptic channels.
- vibrationEffect = null;
- }
- }
- VibrationEffectPlayer vibrationEffectPlayer = (vibrationEffect == null) ? null :
- new VibrationEffectPlayer(
- vibrationEffect, audioAttributes, vibrator, initialLooping);
- if (isVibrationOnly && vibrationEffectPlayer != null) {
- // Abandon the media player now that it's confirmed to not have haptic channels.
- mediaPlayer.release();
- return vibrationEffectPlayer;
- }
- return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager,
- hapticGenerator, volumeShaper, vibrationEffectPlayer);
- } catch (SecurityException | IOException e) {
- if (hapticGenerator != null) {
- hapticGenerator.release();
- }
- // volume shaper closes with media player
- mediaPlayer.release();
- return null;
- } finally {
- Trace.endSection();
- }
- }
-
- /**
- * Creates a {@link LocalRingtonePlayer} for an externally referenced file descriptor. This is
- * intended for loading a fallback from an internal resource, rather than via a Uri.
- */
- @Nullable
- static LocalRingtonePlayer createForFallback(
- @NonNull AudioManager audioManager, @NonNull Vibrator vibrator,
- @NonNull AssetFileDescriptor afd,
- @NonNull AudioAttributes audioAttributes,
- @Nullable VibrationEffect vibrationEffect,
- @NonNull Ringtone.Injectables injectables,
- @Nullable VolumeShaper.Configuration volumeShaperConfig,
- @Nullable AudioDeviceInfo preferredDevice,
- boolean initialLooping, float initialVolume) {
- // Haptic generator not supported for fallback.
- Objects.requireNonNull(audioManager);
- Objects.requireNonNull(afd);
- Objects.requireNonNull(audioAttributes);
- Trace.beginSection("createFallbackLocalMediaPlayer");
-
- MediaPlayer mediaPlayer = injectables.newMediaPlayer();
- try {
- if (afd.getDeclaredLength() < 0) {
- mediaPlayer.setDataSource(afd.getFileDescriptor());
- } else {
- mediaPlayer.setDataSource(afd.getFileDescriptor(),
- afd.getStartOffset(),
- afd.getDeclaredLength());
- }
- mediaPlayer.setAudioAttributes(audioAttributes);
- mediaPlayer.setPreferredDevice(preferredDevice);
- mediaPlayer.setLooping(initialLooping);
- mediaPlayer.setVolume(initialVolume);
- VolumeShaper volumeShaper = null;
- if (volumeShaperConfig != null) {
- volumeShaper = mediaPlayer.createVolumeShaper(volumeShaperConfig);
- }
- mediaPlayer.prepare();
- if (vibrationEffect != null && !audioAttributes.areHapticChannelsMuted()) {
- if (injectables.hasHapticChannels(mediaPlayer)) {
- // Don't play the Vibration effect if the URI has haptic channels.
- vibrationEffect = null;
- }
- }
- VibrationEffectPlayer vibrationEffectPlayer = (vibrationEffect == null) ? null :
- new VibrationEffectPlayer(
- vibrationEffect, audioAttributes, vibrator, initialLooping);
- return new LocalRingtonePlayer(mediaPlayer, audioAttributes, injectables, audioManager,
- /* hapticGenerator= */ null, volumeShaper, vibrationEffectPlayer);
- } catch (SecurityException | IOException e) {
- Log.e(TAG, "Failed to open fallback ringtone");
- // TODO: vibration-effect-only / no-sound LocalRingtonePlayer.
- mediaPlayer.release();
- return null;
- } finally {
- Trace.endSection();
- }
- }
-
- @Override
- public boolean play() {
- // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone
- // (typically because ringer mode is vibrate).
- if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
- == 0 && (mAudioAttributes.areHapticChannelsMuted() || !hasHapticChannels())) {
- maybeStartVibration();
- return true; // Successfully played while muted.
- }
- synchronized (sActiveMediaPlayers) {
- // We keep-alive when a mediaplayer is active, since its finalizer would stop the
- // ringtone. This isn't necessary for vibrations in the vibrator service
- // (i.e. maybeStartVibration in the muted case, above).
- sActiveMediaPlayers.add(this);
- }
-
- mMediaPlayer.setOnCompletionListener(this);
- mMediaPlayer.start();
- if (mVolumeShaper != null) {
- mVolumeShaper.apply(VolumeShaper.Operation.PLAY);
- }
- maybeStartVibration();
- return true;
- }
-
- private void maybeStartVibration() {
- if (mVibrationPlayer != null) {
- mVibrationPlayer.play();
- }
- }
-
- @Override
- public boolean isPlaying() {
- return mMediaPlayer.isPlaying();
- }
-
- @Override
- public void stopAndRelease() {
- synchronized (sActiveMediaPlayers) {
- sActiveMediaPlayers.remove(this);
- }
- try {
- mMediaPlayer.stop();
- } finally {
- if (mVibrationPlayer != null) {
- try {
- mVibrationPlayer.stopAndRelease();
- } catch (Exception e) {
- Log.e(TAG, "Exception stopping ringtone vibration", e);
- }
- }
- if (mHapticGenerator != null) {
- mHapticGenerator.release();
- }
- mMediaPlayer.setOnCompletionListener(null);
- mMediaPlayer.reset();
- mMediaPlayer.release();
- }
- }
-
- @Override
- public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
- mMediaPlayer.setPreferredDevice(audioDeviceInfo);
- }
-
- @Override
- public void setLooping(boolean looping) {
- boolean wasLooping = mMediaPlayer.isLooping();
- if (wasLooping == looping) {
- return;
- }
- mMediaPlayer.setLooping(looping);
- if (mVibrationPlayer != null) {
- mVibrationPlayer.setLooping(looping);
- }
- }
-
- @Override
- public void setHapticGeneratorEnabled(boolean enabled) {
- if (mVibrationPlayer != null) {
- // Ignore haptic generator changes if a vibration player is present. The decision to
- // use one or the other happens before this object is constructed.
- return;
- }
- if (enabled && mHapticGenerator == null && !hasHapticChannels()) {
- mHapticGenerator = mInjectables.createHapticGenerator(mMediaPlayer);
- }
- if (mHapticGenerator != null) {
- mHapticGenerator.setEnabled(enabled);
- }
- }
-
- @Override
- public void setVolume(float volume) {
- mMediaPlayer.setVolume(volume);
- // no effect on vibration player
- }
-
- @Override
- public boolean hasHapticChannels() {
- return mInjectables.hasHapticChannels(mMediaPlayer);
- }
-
- @Override
- public void onCompletion(MediaPlayer mp) {
- synchronized (sActiveMediaPlayers) {
- sActiveMediaPlayers.remove(this);
- }
- mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
- // No effect on vibration: either it's looping and this callback only happens when stopped,
- // or it's not looping, in which case the vibration should play to its own completion.
- }
-
- /** A RingtonePlayer that only plays a VibrationEffect. */
- static class VibrationEffectPlayer implements RingtoneV2.RingtonePlayer {
- private static final int VIBRATION_LOOP_DELAY_MS = 200;
- private final VibrationEffect mVibrationEffect;
- private final VibrationAttributes mVibrationAttributes;
- private final Vibrator mVibrator;
- private boolean mIsLooping;
- private boolean mStartedVibration;
-
- VibrationEffectPlayer(@NonNull VibrationEffect vibrationEffect,
- @NonNull AudioAttributes audioAttributes,
- @NonNull Vibrator vibrator, boolean initialLooping) {
- mVibrationEffect = vibrationEffect;
- mVibrationAttributes = new VibrationAttributes.Builder(audioAttributes).build();
- mVibrator = vibrator;
- mIsLooping = initialLooping;
- }
-
- @Override
- public boolean play() {
- if (!mStartedVibration) {
- try {
- // Adjust the vibration effect to loop.
- VibrationEffect loopAdjustedEffect =
- mVibrationEffect.applyRepeatingIndefinitely(
- mIsLooping, VIBRATION_LOOP_DELAY_MS);
- mVibrator.vibrate(loopAdjustedEffect, mVibrationAttributes);
- mStartedVibration = true;
- } catch (Exception e) {
- // Catch exceptions widely, because we don't want to "leak" looping sounds or
- // vibrations if something goes wrong.
- Log.e(TAG, "Problem starting " + (mIsLooping ? "looping " : "") + "vibration "
- + "for ringtone: " + mVibrationEffect, e);
- return false;
- }
- }
- return true;
- }
-
- @Override
- public boolean isPlaying() {
- return mStartedVibration;
- }
-
- @Override
- public void stopAndRelease() {
- if (mStartedVibration) {
- try {
- mVibrator.cancel(mVibrationAttributes.getUsage());
- mStartedVibration = false;
- } catch (Exception e) {
- // Catch exceptions widely, because we don't want to "leak" looping sounds or
- // vibrations if something goes wrong.
- Log.e(TAG, "Problem stopping vibration for ringtone", e);
- }
- }
- }
-
- @Override
- public void setPreferredDevice(AudioDeviceInfo audioDeviceInfo) {
- // no-op
- }
-
- @Override
- public void setLooping(boolean looping) {
- if (looping == mIsLooping) {
- return;
- }
- mIsLooping = looping;
- if (mStartedVibration) {
- if (!mIsLooping) {
- // Was looping, stop looping
- stopAndRelease();
- }
- // Else was not looping, but can't interfere with a running vibration without
- // restarting it, and don't know if it was finished. So do nothing: apps shouldn't
- // toggle looping after calling play anyway.
- }
- }
-
- @Override
- public void setHapticGeneratorEnabled(boolean enabled) {
- // n/a
- }
-
- @Override
- public void setVolume(float volume) {
- // n/a
- }
-
- @Override
- public boolean hasHapticChannels() {
- return false;
- }
- }
-}
diff --git a/media/java/android/media/OWNERS b/media/java/android/media/OWNERS
index 058c5be..49890c1 100644
--- a/media/java/android/media/OWNERS
+++ b/media/java/android/media/OWNERS
@@ -11,6 +11,3 @@
per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IMediaHTTPService.aidl,JetPlayer.java,MediaDataSource.java,MediaExtractor.java,MediaHTTPConnection.java,MediaHTTPService.java,MediaPlayer.java=set noparent
per-file ExifInterface.java,ExifInterfaceUtils.java,IMediaHTTPConnection.aidl,IMediaHTTPService.aidl,JetPlayer.java,MediaDataSource.java,MediaExtractor.java,MediaHTTPConnection.java,MediaHTTPService.java,MediaPlayer.java=file:platform/frameworks/av:/media/janitors/media_solutions_OWNERS
-
-# Haptics team also works on Ringtone
-per-file *Ringtone* = file:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java
index 8800dc8..e78dc31 100644
--- a/media/java/android/media/Ringtone.java
+++ b/media/java/android/media/Ringtone.java
@@ -16,31 +16,27 @@
package android.media;
-import android.Manifest;
-import android.annotation.IntDef;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
-import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.media.audiofx.HapticGenerator;
import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
import android.os.RemoteException;
import android.os.Trace;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
import android.util.Log;
-
import com.android.internal.annotations.VisibleForTesting;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
+import java.io.IOException;
+import java.util.ArrayList;
/**
* Ringtone provides a quick method for playing a ringtone, notification, or
@@ -53,39 +49,7 @@
*/
public class Ringtone {
private static final String TAG = "Ringtone";
-
- /**
- * The ringtone should only play sound. Any vibration is managed externally.
- * @hide
- */
- public static final int MEDIA_SOUND = 1;
- /**
- * The ringtone should only play vibration. Any sound is managed externally.
- * Requires the {@link android.Manifest.permission#VIBRATE} permission.
- * @hide
- */
- public static final int MEDIA_VIBRATION = 1 << 1;
- /**
- * The ringtone should play sound and vibration.
- * @hide
- */
- public static final int MEDIA_SOUND_AND_VIBRATION = MEDIA_SOUND | MEDIA_VIBRATION;
-
- // This is not a public value, because apps shouldn't enable "all" media - that wouldn't be
- // safe if new media types were added.
- static final int MEDIA_ALL = MEDIA_SOUND | MEDIA_VIBRATION;
-
- /**
- * Declares the types of media that this Ringtone is allowed to play.
- * @hide
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = "MEDIA_", value = {
- MEDIA_SOUND,
- MEDIA_VIBRATION,
- MEDIA_SOUND_AND_VIBRATION,
- })
- public @interface RingtoneMedia {}
+ private static final boolean LOGD = true;
private static final String[] MEDIA_COLUMNS = new String[] {
MediaStore.Audio.Media._ID,
@@ -95,70 +59,51 @@
private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR "
+ MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')";
- // Flag-selected ringtone implementation to use.
- private final ApiInterface mApiImpl;
+ // keep references on active Ringtones until stopped or completion listener called.
+ private static final ArrayList<Ringtone> sActiveRingtones = new ArrayList<Ringtone>();
+
+ private final Context mContext;
+ private final AudioManager mAudioManager;
+ private VolumeShaper.Configuration mVolumeShaperConfig;
+ private VolumeShaper mVolumeShaper;
+
+ /**
+ * Flag indicating if we're allowed to fall back to remote playback using
+ * {@link #mRemotePlayer}. Typically this is false when we're the remote
+ * player and there is nobody else to delegate to.
+ */
+ private final boolean mAllowRemote;
+ private final IRingtonePlayer mRemotePlayer;
+ private final Binder mRemoteToken;
+
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private MediaPlayer mLocalPlayer;
+ private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener();
+ private HapticGenerator mHapticGenerator;
+
+ @UnsupportedAppUsage
+ private Uri mUri;
+ private String mTitle;
+
+ private AudioAttributes mAudioAttributes = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build();
+ private boolean mPreferBuiltinDevice;
+ // playback properties, use synchronized with mPlaybackSettingsLock
+ private boolean mIsLooping = false;
+ private float mVolume = 1.0f;
+ private boolean mHapticGeneratorEnabled = false;
+ private final Object mPlaybackSettingsLock = new Object();
/** {@hide} */
@UnsupportedAppUsage
public Ringtone(Context context, boolean allowRemote) {
- mApiImpl = new RingtoneV1(context, allowRemote);
- }
-
- /**
- * Constructor for legacy V1 initialization paths using non-public APIs on RingtoneV1.
- */
- private Ringtone(RingtoneV1 ringtoneV1) {
- mApiImpl = ringtoneV1;
- }
-
- private Ringtone(Builder builder, @Ringtone.RingtoneMedia int effectiveEnabledMedia,
- @NonNull AudioAttributes effectiveAudioAttributes,
- @Nullable VibrationEffect effectiveVibrationEffect,
- boolean effectiveHapticGeneratorEnabled) {
- mApiImpl = new RingtoneV2(builder.mContext, builder.mInjectables, builder.mAllowRemote,
- effectiveEnabledMedia, builder.mUri, effectiveAudioAttributes,
- builder.mUseExactAudioAttributes, builder.mVolumeShaperConfig,
- builder.mPreferBuiltinDevice, builder.mInitialSoundVolume, builder.mLooping,
- effectiveHapticGeneratorEnabled, effectiveVibrationEffect);
- }
-
- /**
- * Temporary V1 constructor for legacy V1 paths with audio attributes.
- * @hide
- */
- public static Ringtone createV1WithCustomAudioAttributes(
- Context context, AudioAttributes audioAttributes, Uri uri,
- VolumeShaper.Configuration volumeShaperConfig, boolean allowRemote) {
- RingtoneV1 ringtoneV1 = new RingtoneV1(context, allowRemote);
- ringtoneV1.setAudioAttributesField(audioAttributes);
- ringtoneV1.setUri(uri, volumeShaperConfig);
- ringtoneV1.reinitializeActivePlayer();
- return new Ringtone(ringtoneV1);
- }
-
- /**
- * Temporary V1 constructor for legacy V1 paths with stream type.
- * @hide
- */
- public static Ringtone createV1WithCustomStreamType(
- Context context, int streamType, Uri uri,
- VolumeShaper.Configuration volumeShaperConfig) {
- RingtoneV1 ringtoneV1 = new RingtoneV1(context, /* allowRemote= */ true);
- if (streamType >= 0) {
- ringtoneV1.setStreamType(streamType);
- }
- ringtoneV1.setUri(uri, volumeShaperConfig);
- if (!ringtoneV1.reinitializeActivePlayer()) {
- Log.e(TAG, "Failed to open ringtone " + uri);
- return null;
- }
- return new Ringtone(ringtoneV1);
- }
-
- /** @hide */
- @RingtoneMedia
- public int getEnabledMedia() {
- return mApiImpl.getEnabledMedia();
+ mContext = context;
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ mAllowRemote = allowRemote;
+ mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
+ mRemoteToken = allowRemote ? new Binder() : null;
}
/**
@@ -169,7 +114,10 @@
*/
@Deprecated
public void setStreamType(int streamType) {
- mApiImpl.setStreamType(streamType);
+ PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", "setStreamType()");
+ setAudioAttributes(new AudioAttributes.Builder()
+ .setInternalLegacyStreamType(streamType)
+ .build());
}
/**
@@ -181,7 +129,7 @@
*/
@Deprecated
public int getStreamType() {
- return mApiImpl.getStreamType();
+ return AudioAttributes.toLegacyStreamType(mAudioAttributes);
}
/**
@@ -190,45 +138,54 @@
*/
public void setAudioAttributes(AudioAttributes attributes)
throws IllegalArgumentException {
- mApiImpl.setAudioAttributes(attributes);
+ setAudioAttributesField(attributes);
+ // The audio attributes have to be set before the media player is prepared.
+ // Re-initialize it.
+ setUri(mUri, mVolumeShaperConfig);
+ createLocalMediaPlayer();
}
/**
- * Returns the vibration effect that this ringtone was created with, if vibration is enabled.
- * Otherwise, returns null.
+ * Same as {@link #setAudioAttributes(AudioAttributes)} except this one does not create
+ * the media player.
* @hide
*/
- @Nullable
- public VibrationEffect getVibrationEffect() {
- return mApiImpl.getVibrationEffect();
- }
-
- /** @hide */
- @VisibleForTesting
- public boolean getPreferBuiltinDevice() {
- return mApiImpl.getPreferBuiltinDevice();
- }
-
- /** @hide */
- @VisibleForTesting
- public VolumeShaper.Configuration getVolumeShaperConfig() {
- return mApiImpl.getVolumeShaperConfig();
+ public void setAudioAttributesField(@Nullable AudioAttributes attributes) {
+ if (attributes == null) {
+ throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
+ }
+ mAudioAttributes = attributes;
}
/**
- * Returns whether this player is local only, or can defer to the remote player. The
- * result may differ from the builder if there is no remote player available at all.
- * @hide
+ * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is
+ * the one on which outgoing audio for SIM calls is played.
+ *
+ * @param audioManager the audio manage.
+ * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if
+ * none can be found.
*/
- @VisibleForTesting
- public boolean isLocalOnly() {
- return mApiImpl.isLocalOnly();
+ private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) {
+ AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo device : deviceList) {
+ if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ return device;
+ }
+ }
+ return null;
}
- /** @hide */
- @VisibleForTesting
- public boolean isUsingRemotePlayer() {
- return mApiImpl.isUsingRemotePlayer();
+ /**
+ * Sets the preferred device of the ringtong playback to the built-in device.
+ *
+ * @hide
+ */
+ public boolean preferBuiltinDevice(boolean enable) {
+ mPreferBuiltinDevice = enable;
+ if (mLocalPlayer == null) {
+ return true;
+ }
+ return mLocalPlayer.setPreferredDevice(getBuiltinDevice(mAudioManager));
}
/**
@@ -237,16 +194,76 @@
* false if it did not succeed and can't be tried remotely.
* @hide
*/
- public boolean reinitializeActivePlayer() {
- return mApiImpl.reinitializeActivePlayer();
+ public boolean createLocalMediaPlayer() {
+ Trace.beginSection("createLocalMediaPlayer");
+ if (mUri == null) {
+ Log.e(TAG, "Could not create media player as no URI was provided.");
+ return mAllowRemote && mRemotePlayer != null;
+ }
+ destroyLocalPlayer();
+ // try opening uri locally before delegating to remote player
+ mLocalPlayer = new MediaPlayer();
+ try {
+ mLocalPlayer.setDataSource(mContext, mUri);
+ mLocalPlayer.setAudioAttributes(mAudioAttributes);
+ mLocalPlayer.setPreferredDevice(
+ mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null);
+ synchronized (mPlaybackSettingsLock) {
+ applyPlaybackProperties_sync();
+ }
+ if (mVolumeShaperConfig != null) {
+ mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
+ }
+ mLocalPlayer.prepare();
+
+ } catch (SecurityException | IOException e) {
+ destroyLocalPlayer();
+ if (!mAllowRemote) {
+ Log.w(TAG, "Remote playback not allowed: " + e);
+ }
+ }
+
+ if (LOGD) {
+ if (mLocalPlayer != null) {
+ Log.d(TAG, "Successfully created local player");
+ } else {
+ Log.d(TAG, "Problem opening; delegating to remote player");
+ }
+ }
+ Trace.endSection();
+ return mLocalPlayer != null || (mAllowRemote && mRemotePlayer != null);
}
/**
* Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
+ * If the ringtone has not been created, it will load based on URI provided at {@link #setUri}
+ * and if not URI has been set, it will assume no haptic channels are present.
* @hide
*/
public boolean hasHapticChannels() {
- return mApiImpl.hasHapticChannels();
+ // FIXME: support remote player, or internalize haptic channels support and remove entirely.
+ try {
+ android.os.Trace.beginSection("Ringtone.hasHapticChannels");
+ if (mLocalPlayer != null) {
+ for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) {
+ if (trackInfo.hasHapticChannels()) {
+ return true;
+ }
+ }
+ }
+ } finally {
+ android.os.Trace.endSection();
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether a local player has been created for this ringtone.
+ * @hide
+ */
+ @VisibleForTesting
+ public boolean hasLocalPlayer() {
+ return mLocalPlayer != null;
}
/**
@@ -255,7 +272,7 @@
* {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set.
*/
public AudioAttributes getAudioAttributes() {
- return mApiImpl.getAudioAttributes();
+ return mAudioAttributes;
}
/**
@@ -263,7 +280,10 @@
* @param looping whether to loop or not.
*/
public void setLooping(boolean looping) {
- mApiImpl.setLooping(looping);
+ synchronized (mPlaybackSettingsLock) {
+ mIsLooping = looping;
+ applyPlaybackProperties_sync();
+ }
}
/**
@@ -271,7 +291,9 @@
* @return true if this player loops when playing.
*/
public boolean isLooping() {
- return mApiImpl.isLooping();
+ synchronized (mPlaybackSettingsLock) {
+ return mIsLooping;
+ }
}
/**
@@ -280,7 +302,12 @@
* corresponds to no attenuation being applied.
*/
public void setVolume(float volume) {
- mApiImpl.setVolume(volume);
+ synchronized (mPlaybackSettingsLock) {
+ if (volume < 0.0f) { volume = 0.0f; }
+ if (volume > 1.0f) { volume = 1.0f; }
+ mVolume = volume;
+ applyPlaybackProperties_sync();
+ }
}
/**
@@ -288,7 +315,9 @@
* @return a value between 0.0f and 1.0f.
*/
public float getVolume() {
- return mApiImpl.getVolume();
+ synchronized (mPlaybackSettingsLock) {
+ return mVolume;
+ }
}
/**
@@ -299,7 +328,14 @@
* @see android.media.audiofx.HapticGenerator#isAvailable()
*/
public boolean setHapticGeneratorEnabled(boolean enabled) {
- return mApiImpl.setHapticGeneratorEnabled(enabled);
+ if (!HapticGenerator.isAvailable()) {
+ return false;
+ }
+ synchronized (mPlaybackSettingsLock) {
+ mHapticGeneratorEnabled = enabled;
+ applyPlaybackProperties_sync();
+ }
+ return true;
}
/**
@@ -307,7 +343,35 @@
* @return true if the HapticGenerator is enabled.
*/
public boolean isHapticGeneratorEnabled() {
- return mApiImpl.isHapticGeneratorEnabled();
+ synchronized (mPlaybackSettingsLock) {
+ return mHapticGeneratorEnabled;
+ }
+ }
+
+ /**
+ * Must be called synchronized on mPlaybackSettingsLock
+ */
+ private void applyPlaybackProperties_sync() {
+ if (mLocalPlayer != null) {
+ mLocalPlayer.setVolume(mVolume);
+ mLocalPlayer.setLooping(mIsLooping);
+ if (mHapticGenerator == null && mHapticGeneratorEnabled) {
+ mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId());
+ }
+ if (mHapticGenerator != null) {
+ mHapticGenerator.setEnabled(mHapticGeneratorEnabled);
+ }
+ } else if (mAllowRemote && (mRemotePlayer != null)) {
+ try {
+ mRemotePlayer.setPlaybackProperties(
+ mRemoteToken, mVolume, mIsLooping, mHapticGeneratorEnabled);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem setting playback properties: ", e);
+ }
+ } else {
+ Log.w(TAG,
+ "Neither local nor remote player available when applying playback properties");
+ }
}
/**
@@ -317,7 +381,8 @@
* @param context A context used for querying.
*/
public String getTitle(Context context) {
- return mApiImpl.getTitle(context);
+ if (mTitle != null) return mTitle;
+ return mTitle = getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
}
/**
@@ -391,24 +456,126 @@
return title;
}
+ /**
+ * Set {@link Uri} to be used for ringtone playback.
+ * {@link IRingtonePlayer}.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void setUri(Uri uri) {
+ setUri(uri, null);
+ }
+
+ /**
+ * @hide
+ */
+ public void setVolumeShaperConfig(@Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ mVolumeShaperConfig = volumeShaperConfig;
+ }
+
+ /**
+ * Set {@link Uri} to be used for ringtone playback. Attempts to open
+ * locally, otherwise will delegate playback to remote
+ * {@link IRingtonePlayer}. Add {@link VolumeShaper} if required.
+ *
+ * @hide
+ */
+ public void setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ mVolumeShaperConfig = volumeShaperConfig;
+ mUri = uri;
+ if (mUri == null) {
+ destroyLocalPlayer();
+ }
+ }
+
/** {@hide} */
@UnsupportedAppUsage
public Uri getUri() {
- return mApiImpl.getUri();
+ return mUri;
}
/**
* Plays the ringtone.
*/
public void play() {
- mApiImpl.play();
+ if (mLocalPlayer != null) {
+ // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone
+ // (typically because ringer mode is vibrate).
+ if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
+ != 0) {
+ startLocalPlayer();
+ } else if (!mAudioAttributes.areHapticChannelsMuted() && hasHapticChannels()) {
+ // is haptic only ringtone
+ startLocalPlayer();
+ }
+ } else if (mAllowRemote && (mRemotePlayer != null) && (mUri != null)) {
+ final Uri canonicalUri = mUri.getCanonicalUri();
+ final boolean looping;
+ final float volume;
+ synchronized (mPlaybackSettingsLock) {
+ looping = mIsLooping;
+ volume = mVolume;
+ }
+ try {
+ mRemotePlayer.playWithVolumeShaping(mRemoteToken, canonicalUri, mAudioAttributes,
+ volume, looping, mVolumeShaperConfig);
+ } catch (RemoteException e) {
+ if (!playFallbackRingtone()) {
+ Log.w(TAG, "Problem playing ringtone: " + e);
+ }
+ }
+ } else {
+ if (!playFallbackRingtone()) {
+ Log.w(TAG, "Neither local nor remote playback available");
+ }
+ }
}
/**
* Stops a playing ringtone.
*/
public void stop() {
- mApiImpl.stop();
+ if (mLocalPlayer != null) {
+ destroyLocalPlayer();
+ } else if (mAllowRemote && (mRemotePlayer != null)) {
+ try {
+ mRemotePlayer.stop(mRemoteToken);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem stopping ringtone: " + e);
+ }
+ }
+ }
+
+ private void destroyLocalPlayer() {
+ if (mLocalPlayer != null) {
+ if (mHapticGenerator != null) {
+ mHapticGenerator.release();
+ mHapticGenerator = null;
+ }
+ mLocalPlayer.setOnCompletionListener(null);
+ mLocalPlayer.reset();
+ mLocalPlayer.release();
+ mLocalPlayer = null;
+ mVolumeShaper = null;
+ synchronized (sActiveRingtones) {
+ sActiveRingtones.remove(this);
+ }
+ }
+ }
+
+ private void startLocalPlayer() {
+ if (mLocalPlayer == null) {
+ return;
+ }
+ synchronized (sActiveRingtones) {
+ sActiveRingtones.add(this);
+ }
+ mLocalPlayer.setOnCompletionListener(mCompletionListener);
+ mLocalPlayer.start();
+ if (mVolumeShaper != null) {
+ mVolumeShaper.apply(VolumeShaper.Operation.PLAY);
+ }
}
/**
@@ -417,353 +584,87 @@
* @return True if playing, false otherwise.
*/
public boolean isPlaying() {
- return mApiImpl.isPlaying();
- }
-
- /**
- * Build a {@link Ringtone} to easily play sounds for ringtones, alarms and notifications.
- *
- * TODO: when un-hide, deprecate Ringtone: setAudioAttributes, setLooping,
- * setHapticGeneratorEnabled (no-effect if MEDIA_VIBRATION),
- * static RingtoneManager.getRingtone.
- * @hide
- */
- public static final class Builder {
- private final Context mContext;
- private final int mEnabledMedia;
- private Uri mUri;
- private final AudioAttributes mAudioAttributes;
- private boolean mUseExactAudioAttributes = false;
- // Not a static default since it doesn't really need to be in memory forever.
- private Injectables mInjectables = new Injectables();
- private VolumeShaper.Configuration mVolumeShaperConfig;
- private boolean mPreferBuiltinDevice = false;
- private boolean mAllowRemote = true;
- private boolean mHapticGeneratorEnabled = false;
- private float mInitialSoundVolume = 1.0f;
- private boolean mLooping = false;
- private VibrationEffect mVibrationEffect;
-
- /**
- * Constructs a builder to play the given media types from the mediaUri. If the mediaUri
- * is null (for example, an unset-setting), then fallback logic will dictate what plays.
- *
- * <p>When built, if the ringtone is already known to be a no-op, such as explicitly
- * silent, then the {@link #build} may return null.
- *
- * @param context The context for playing the ringtone.
- * @param enabledMedia Which media to play. Media not included is implicitly muted. Device
- * settings such as volume and vibrate-only may also affect which
- * media is played.
- * @param audioAttributes The attributes to use for playback, which affects the volumes and
- * settings that are applied.
- */
- public Builder(@NonNull Context context, @RingtoneMedia int enabledMedia,
- @NonNull AudioAttributes audioAttributes) {
- mContext = context;
- mEnabledMedia = enabledMedia;
- mAudioAttributes = audioAttributes;
- }
-
- /**
- * Inject test intercepters for static methods.
- * @hide
- */
- @NonNull
- public Builder setInjectables(Injectables injectables) {
- mInjectables = injectables;
- return this;
- }
-
- /**
- * The uri for the ringtone media to play. This is typically a user's preference for the
- * sound. If null, then it is treated as though the user's preference is unset and
- * fallback behavior, such as using the default ringtone setting, are used instead.
- *
- * When sound media is enabled, this is assumed to be a sound URI.
- */
- @NonNull
- public Builder setUri(@Nullable Uri uri) {
- mUri = uri;
- return this;
- }
-
- /**
- * Sets the VibrationEffect to use if vibration is enabled on this ringtone. The caller
- * should use {@link android.os.Vibrator#areVibrationFeaturesSupported} to ensure
- * that the effect is usable on this device, otherwise system defaults will be used.
- *
- * <p>Vibration will only happen if the Builder was created with media type
- * {@link Ringtone#MEDIA_VIBRATION} or {@link Ringtone#MEDIA_SOUND_AND_VIBRATION}, and
- * the application has the {@link android.Manifest.permission#VIBRATE} permission.
- *
- * <p>If the Ringtone is looping when it is played, then the VibrationEffect will be
- * modified to loop. Similarly, if the ringtone is not looping, a repeating
- * VibrationEffect will be modified to be non-repeating when the ringtone is played. Calls
- * to {@link Ringtone#setLooping} after the ringtone has started playing will stop a looping
- * vibration, but has no effect otherwise: specifically it will not restart vibration.
- */
- @NonNull
- public Builder setVibrationEffect(@NonNull VibrationEffect effect) {
- mVibrationEffect = effect;
- return this;
- }
-
- /**
- * Sets whether the resulting ringtone should loop until {@link Ringtone#stop()} is called,
- * or just play once.
- */
- @NonNull
- public Builder setLooping(boolean looping) {
- mLooping = looping;
- return this;
- }
-
- /**
- * Sets the VolumeShaper.Configuration to apply to the ringtone.
- * @hide
- */
- @NonNull
- public Builder setVolumeShaperConfig(
- @Nullable VolumeShaper.Configuration volumeShaperConfig) {
- mVolumeShaperConfig = volumeShaperConfig;
- return this;
- }
-
- /**
- * Whether to enable or disable the haptic generator.
- * @hide
- */
- @NonNull
- public Builder setEnableHapticGenerator(boolean enabled) {
- // Note that this property is mutable (but deprecated) on the Ringtone class itself.
- mHapticGeneratorEnabled = enabled;
- return this;
- }
-
- /**
- * Sets the initial sound volume for the ringtone.
- */
- @NonNull
- public Builder setInitialSoundVolume(float initialSoundVolume) {
- mInitialSoundVolume = initialSoundVolume;
- return this;
- }
-
- /**
- * Sets the preferred device of the ringtone playback to the built-in device. This is
- * only for use by the system server with known-good Uris.
- * @hide
- */
- @NonNull
- public Builder setPreferBuiltinDevice() {
- mPreferBuiltinDevice = true;
- mAllowRemote = false; // Already in system.
- return this;
- }
-
- /**
- * Indicates that {@link AudioAttributes#areHapticChannelsMuted()} on the builder's
- * AudioAttributes should not be overridden. This is used to enable legacy behavior of
- * calling {@link Ringtone#setAudioAttributes} on an already-created ringtone, and can in
- * turn cause vibration during a "sound-only" session or can suppress audio-coupled
- * haptics that would usually take priority (therefore potentially falling back to
- * the VibrationEffect or system defaults).
- *
- * <p>Without this setting, the haptic channels will be automatically muted or not by the
- * Ringtone according to whether vibration is enabled or not.
- *
- * <p>This is for internal-use only. New applications should configure the vibration
- * behavior explicitly with the (TODO: future RingtoneSetting.setVibrationSource).
- * Handling haptic channels outside Ringtone leads to extra loads of the sound uri.
- * @hide
- */
- @NonNull
- public Builder setUseExactAudioAttributes(boolean useExactAttrs) {
- mUseExactAudioAttributes = useExactAttrs;
- return this;
- }
-
- /**
- * Prevent fallback to the remote service. This is primarily intended for use within the
- * remote IRingtonePlayer service itself, to avoid loops.
- * @hide
- */
- @NonNull
- public Builder setLocalOnly() {
- mAllowRemote = false;
- return this;
- }
-
- private boolean isVibrationEnabledAndAvailable() {
- if ((mEnabledMedia & MEDIA_VIBRATION) == 0) {
- return false;
- }
- Vibrator vibrator = mContext.getSystemService(Vibrator.class);
- if (!vibrator.hasVibrator()) {
- return false;
- }
- if (mContext.checkSelfPermission(Manifest.permission.VIBRATE)
- != PackageManager.PERMISSION_GRANTED) {
- Log.w(TAG, "Ringtone requests vibration enabled, but no VIBRATE permission");
- return false;
- }
- return true;
- }
-
- /**
- * Returns the built Ringtone, or null if there was a problem loading the Uri and there
- * are no fallback options available.
- */
- @Nullable
- public Ringtone build() {
- @Ringtone.RingtoneMedia int effectiveEnabledMedia = mEnabledMedia;
- VibrationEffect effectiveVibrationEffect = mVibrationEffect;
-
- // Normalize media to that supported on this SDK level.
- if (effectiveEnabledMedia != (effectiveEnabledMedia & MEDIA_ALL)) {
- Log.e(TAG, "Unsupported media type: " + effectiveEnabledMedia);
- effectiveEnabledMedia = effectiveEnabledMedia & MEDIA_ALL;
- }
- final boolean effectiveHapticGenerator;
- final boolean hapticChannelsSupported;
- AudioAttributes effectiveAudioAttributes = mAudioAttributes;
- final boolean hapticChannelsMuted = mAudioAttributes.areHapticChannelsMuted();
- if (!isVibrationEnabledAndAvailable()) {
- // Vibration isn't active: turn off everything that might cause extra work.
- effectiveEnabledMedia &= ~MEDIA_VIBRATION;
- effectiveHapticGenerator = false;
- effectiveVibrationEffect = null;
- if (!mUseExactAudioAttributes && !hapticChannelsMuted) {
- effectiveAudioAttributes = new AudioAttributes.Builder(effectiveAudioAttributes)
- .setHapticChannelsMuted(true)
- .build();
- }
- } else {
- // Vibration is active.
- effectiveHapticGenerator =
- mHapticGeneratorEnabled && mInjectables.isHapticGeneratorAvailable();
- hapticChannelsSupported = mInjectables.isHapticPlaybackSupported();
- // Haptic channels are preferred if they are available, and not explicitly muted.
- // We won't know if haptic channels are available until loading the media player,
- // and since the media player needs to be reset to change audio attributes, then
- // we proactively enable the channels - it won't matter if they aren't present.
- if (!mUseExactAudioAttributes) {
- boolean shouldBeMuted = effectiveHapticGenerator || !hapticChannelsSupported;
- if (shouldBeMuted != hapticChannelsMuted) {
- effectiveAudioAttributes =
- new AudioAttributes.Builder(effectiveAudioAttributes)
- .setHapticChannelsMuted(shouldBeMuted)
- .build();
- }
- }
- // If no contextual vibration, then try loading the default one for the URI.
- if (mVibrationEffect == null && mUri != null) {
- effectiveVibrationEffect = VibrationEffect.get(mUri, mContext);
- }
- }
+ if (mLocalPlayer != null) {
+ return mLocalPlayer.isPlaying();
+ } else if (mAllowRemote && (mRemotePlayer != null)) {
try {
- Ringtone ringtone = new Ringtone(this, effectiveEnabledMedia,
- effectiveAudioAttributes, effectiveVibrationEffect,
- effectiveHapticGenerator);
- if (ringtone.reinitializeActivePlayer()) {
- return ringtone;
- } else {
- Log.e(TAG, "Failed to open ringtone " + mUri);
- return null;
- }
- } catch (Exception ex) {
- // Catching Exception isn't great, but was done in the old
- // RingtoneManager.getRingtone and hides errors like DocumentsProvider throwing
- // IllegalArgumentException instead of FileNotFoundException, and also robolectric
- // failures when ShadowMediaPlayer wasn't pre-informed of the ringtone.
- Log.e(TAG, "Failed while opening ringtone " + mUri, ex);
- return null;
+ return mRemotePlayer.isPlaying(mRemoteToken);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Problem checking ringtone: " + e);
+ return false;
}
- }
- }
-
- /**
- * Interface for intercepting static methods and constructors, for unit testing only.
- * @hide
- */
- @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
- public static class Injectables {
- /** Intercept {@code new MediaPlayer()}. */
- @NonNull
- public MediaPlayer newMediaPlayer() {
- return new MediaPlayer();
- }
-
- /** Intercept {@link HapticGenerator#isAvailable}. */
- public boolean isHapticGeneratorAvailable() {
- return HapticGenerator.isAvailable();
- }
-
- /**
- * Intercept {@link HapticGenerator#create} using
- * {@link MediaPlayer#getAudioSessionId()} from the given media player.
- */
- @Nullable
- public HapticGenerator createHapticGenerator(@NonNull MediaPlayer mediaPlayer) {
- return HapticGenerator.create(mediaPlayer.getAudioSessionId());
- }
-
- /** Returns the result of {@link AudioManager#isHapticPlaybackSupported()}. */
- public boolean isHapticPlaybackSupported() {
- return AudioManager.isHapticPlaybackSupported();
- }
-
- /**
- * Returns whether the MediaPlayer tracks have haptic channels. This is the same as
- * AudioManager.hasHapticChannels, except it uses an already prepared MediaPlayer to avoid
- * loading the metadata a second time.
- */
- public boolean hasHapticChannels(MediaPlayer mp) {
- try {
- Trace.beginSection("Ringtone.hasHapticChannels");
- for (MediaPlayer.TrackInfo trackInfo : mp.getTrackInfo()) {
- if (trackInfo.hasHapticChannels()) {
- return true;
- }
- }
- } finally {
- Trace.endSection();
- }
+ } else {
+ Log.w(TAG, "Neither local nor remote playback available");
return false;
}
-
}
- /**
- * Interface for alternative Ringtone implementations. See the public Ringtone methods that
- * delegate to these for documentation.
- * @hide
- */
- interface ApiInterface {
- void setStreamType(int streamType);
- int getStreamType();
- void setAudioAttributes(AudioAttributes attributes);
- boolean getPreferBuiltinDevice();
- VolumeShaper.Configuration getVolumeShaperConfig();
- boolean isLocalOnly();
- boolean isUsingRemotePlayer();
- boolean reinitializeActivePlayer();
- boolean hasHapticChannels();
- AudioAttributes getAudioAttributes();
- void setLooping(boolean looping);
- boolean isLooping();
- void setVolume(float volume);
- float getVolume();
- boolean setHapticGeneratorEnabled(boolean enabled);
- boolean isHapticGeneratorEnabled();
- String getTitle(Context context);
- Uri getUri();
- void play();
- void stop();
- boolean isPlaying();
- // V2 future-public methods
- @RingtoneMedia int getEnabledMedia();
- VibrationEffect getVibrationEffect();
+ private boolean playFallbackRingtone() {
+ int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes);
+ if (mAudioManager.getStreamVolume(streamType) == 0) {
+ return false;
+ }
+ int ringtoneType = RingtoneManager.getDefaultType(mUri);
+ if (ringtoneType != -1 &&
+ RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) {
+ Log.w(TAG, "not playing fallback for " + mUri);
+ return false;
+ }
+ // Default ringtone, try fallback ringtone.
+ try {
+ AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
+ com.android.internal.R.raw.fallbackring);
+ if (afd == null) {
+ Log.e(TAG, "Could not load fallback ringtone");
+ return false;
+ }
+ mLocalPlayer = new MediaPlayer();
+ if (afd.getDeclaredLength() < 0) {
+ mLocalPlayer.setDataSource(afd.getFileDescriptor());
+ } else {
+ mLocalPlayer.setDataSource(afd.getFileDescriptor(),
+ afd.getStartOffset(),
+ afd.getDeclaredLength());
+ }
+ mLocalPlayer.setAudioAttributes(mAudioAttributes);
+ synchronized (mPlaybackSettingsLock) {
+ applyPlaybackProperties_sync();
+ }
+ if (mVolumeShaperConfig != null) {
+ mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
+ }
+ mLocalPlayer.prepare();
+ startLocalPlayer();
+ afd.close();
+ } catch (IOException ioe) {
+ destroyLocalPlayer();
+ Log.e(TAG, "Failed to open fallback ringtone");
+ return false;
+ } catch (NotFoundException nfe) {
+ Log.e(TAG, "Fallback ringtone does not exist");
+ return false;
+ }
+ return true;
+ }
+
+ void setTitle(String title) {
+ mTitle = title;
+ }
+
+ @Override
+ protected void finalize() {
+ if (mLocalPlayer != null) {
+ mLocalPlayer.release();
+ }
+ }
+
+ class MyOnCompletionListener implements MediaPlayer.OnCompletionListener {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ synchronized (sActiveRingtones) {
+ sActiveRingtones.remove(Ringtone.this);
+ }
+ mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
+ }
}
}
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index b5a9ae2..3432b3f 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -16,7 +16,7 @@
package android.media;
-import android.annotation.IntDef;
+import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -30,27 +30,24 @@
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.UserInfo;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.StaleDataException;
import android.net.Uri;
+import android.os.Build;
import android.os.Environment;
import android.os.FileUtils;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
-import android.os.vibrator.Flags;
-import android.os.vibrator.persistence.VibrationXmlParser;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
import android.provider.Settings.System;
-import android.text.TextUtils;
import android.util.Log;
import com.android.internal.database.SortCursor;
@@ -61,8 +58,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
@@ -122,53 +117,6 @@
public static final String ACTION_RINGTONE_PICKER = "android.intent.action.RINGTONE_PICKER";
/**
- * Given to the ringtone picker as a string that represents the category of ringtone picker that
- * should be used. This value should also be returned once a ringtone is selected.
- * <p>
- * The categories are:
- * <li>{@link #CATEGORY_RINGTONE_PICKER_SOUND}
- * <li>{@link #CATEGORY_RINGTONE_PICKER_VIBRATION}
- * <li>{@link #CATEGORY_RINGTONE_PICKER_RINGTONE}
- * <li>{@link Intent#CATEGORY_DEFAULT}
- *
- * <p> If the category is {@link Intent#CATEGORY_DEFAULT} or absent, then the picker will
- * default to a sound-only ringtone picker.
- *
- * <p> If the selected category was not supported, then the returned category will be null.
- *
- * @hide
- */
- public static final String EXTRA_RINGTONE_PICKER_CATEGORY =
- "android.intent.extra.ringtone.RINGTONE_PICKER_CATEGORY";
-
- /**
- * A sound-only ringtone picker.
- *
- * @hide
- * @see #EXTRA_RINGTONE_PICKER_CATEGORY
- */
- public static final String CATEGORY_RINGTONE_PICKER_SOUND =
- "android.net.category.RINGTONE_PICKER_SOUND";
-
- /**
- * A vibration-only ringtone picker.
- *
- * @hide
- * @see #EXTRA_RINGTONE_PICKER_CATEGORY
- */
- public static final String CATEGORY_RINGTONE_PICKER_VIBRATION =
- "android.net.category.RINGTONE_PICKER_VIBRATION";
-
- /**
- * A combined sound and vibration ringtone picker.
- *
- * @hide
- * @see #EXTRA_RINGTONE_PICKER_CATEGORY
- */
- public static final String CATEGORY_RINGTONE_PICKER_RINGTONE =
- "android.net.category.RINGTONE_PICKER_RINGTONE";
-
- /**
* Given to the ringtone picker as a boolean. Whether to show an item for
* "Default".
*
@@ -209,18 +157,6 @@
*/
public static final String EXTRA_RINGTONE_EXISTING_URI =
"android.intent.extra.ringtone.EXISTING_URI";
-
- /**
- * Similar to #EXTRA_RINGTONE_EXISTING_URI but the {@link Uri} can include both sound and
- * vibration.
- * <p>This can include silent sound/vibration explicitly by setting that part of the URI to
- * null.
- *
- * @hide
- * @see #ACTION_RINGTONE_PICKER
- */
- public static final String EXTRA_RINGTONE_EXISTING_RINGTONE_URI =
- "android.intent.extra.ringtone.RINGTONE_EXISTING_RINGTONE_URI";
/**
* Given to the ringtone picker as a {@link Uri}. The {@link Uri} of the
@@ -273,30 +209,21 @@
*/
public static final String EXTRA_RINGTONE_PICKED_URI =
"android.intent.extra.ringtone.PICKED_URI";
-
- /**
- * Declares the allowed types of media for this RingtoneManager.
- * @hide
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = "MEDIA_", value = {
- Ringtone.MEDIA_SOUND,
- Ringtone.MEDIA_VIBRATION,
- })
- public @interface MediaType {}
-
+
// Make sure the column ordering and then ..._COLUMN_INDEX are in sync
- private static final String[] MEDIA_AUDIO_COLUMNS = new String[] {
+ private static final String[] INTERNAL_COLUMNS = new String[] {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.TITLE_KEY,
};
- private static final String[] MEDIA_VIBRATION_COLUMNS = new String[]{
- MediaStore.Files.FileColumns._ID,
- MediaStore.Files.FileColumns.TITLE,
+ private static final String[] MEDIA_COLUMNS = new String[] {
+ MediaStore.Audio.Media._ID,
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.TITLE_KEY,
};
/**
@@ -324,9 +251,7 @@
private Cursor mCursor;
private int mType = TYPE_RINGTONE;
- @MediaType
- private int mMediaType = Ringtone.MEDIA_SOUND;
-
+
/**
* If a column (item from this list) exists in the Cursor, its value must
* be true (value of 1) for the row to be returned.
@@ -393,41 +318,6 @@
}
/**
- * Sets the media type that will be listed by the RingtoneManager.
- *
- * <p>This method should be called before calling {@link RingtoneManager#getCursor()}.
- *
- * @hide
- */
- public void setMediaType(@MediaType int mediaType) {
- if (mCursor != null) {
- throw new IllegalStateException(
- "Setting media should be done before calling getCursor().");
- }
-
- switch (mediaType) {
- case Ringtone.MEDIA_SOUND:
- case Ringtone.MEDIA_VIBRATION:
- mMediaType = mediaType;
- break;
- default:
- throw new IllegalArgumentException("Unsupported media type " + mediaType);
- }
- }
-
- /**
- * Returns the RingtoneManagers media type.
- *
- * @return the media type.
- * @see #setMediaType
- * @hide
- */
- @MediaType
- public int getMediaType() {
- return mMediaType;
- }
-
- /**
* Sets which type(s) of ringtones will be listed by this.
*
* @param type The type(s), one or more of {@link #TYPE_RINGTONE},
@@ -465,25 +355,6 @@
}
}
- /** @hide */
- @NonNull
- public static AudioAttributes getDefaultAudioAttributes(int ringtoneType) {
- AudioAttributes.Builder builder = new AudioAttributes.Builder();
- switch (ringtoneType) {
- case TYPE_ALARM:
- builder.setUsage(AudioAttributes.USAGE_ALARM);
- break;
- case TYPE_NOTIFICATION:
- builder.setUsage(AudioAttributes.USAGE_NOTIFICATION);
- break;
- default: // ringtone or all
- builder.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
- break;
- }
- builder.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION);
- return builder.build();
- }
-
/**
* Whether retrieving another {@link Ringtone} will stop playing the
* previously retrieved {@link Ringtone}.
@@ -564,19 +435,19 @@
return mCursor;
}
- ArrayList<Cursor> cursors = new ArrayList<>();
-
- cursors.add(queryMediaStore(/* internal= */ true));
- cursors.add(queryMediaStore(/* internal= */ false));
+ ArrayList<Cursor> ringtoneCursors = new ArrayList<Cursor>();
+ ringtoneCursors.add(getInternalRingtones());
+ ringtoneCursors.add(getMediaRingtones());
if (mIncludeParentRingtones) {
Cursor parentRingtonesCursor = getParentProfileRingtones();
if (parentRingtonesCursor != null) {
- cursors.add(parentRingtonesCursor);
+ ringtoneCursors.add(parentRingtonesCursor);
}
}
- return mCursor = new SortCursor(cursors.toArray(new Cursor[cursors.size()]),
- getSortOrderForMedia(mMediaType));
+
+ return mCursor = new SortCursor(ringtoneCursors.toArray(new Cursor[ringtoneCursors.size()]),
+ MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
}
private Cursor getParentProfileRingtones() {
@@ -588,7 +459,9 @@
// We don't need to re-add the internal ringtones for the work profile since
// they are the same as the personal profile. We just need the external
// ringtones.
- return queryMediaStore(parentContext, /* internal= */ false);
+ final Cursor res = getMediaRingtones(parentContext);
+ return new ExternalRingtonesCursorWrapper(res, ContentProvider.maybeAddUserId(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, parentInfo.id));
}
}
return null;
@@ -606,32 +479,11 @@
mPreviousRingtone.stop();
}
- Ringtone ringtone;
- Uri positionUri = getRingtoneUri(position);
- if (Flags.hapticsCustomizationRingtoneV2Enabled()) {
- mPreviousRingtone = new Ringtone.Builder(
- mContext, mMediaType, getDefaultAudioAttributes(mType))
- .setUri(positionUri)
- .build();
- } else {
- mPreviousRingtone = createRingtoneV1WithStreamType(mContext, positionUri,
- inferStreamType(), /* volumeShaperConfig= */ null);
- }
+ mPreviousRingtone =
+ getRingtone(mContext, getRingtoneUri(position), inferStreamType(), true);
return mPreviousRingtone;
}
- private static Ringtone createRingtoneV1WithStreamType(
- final Context context, Uri ringtoneUri, int streamType,
- @Nullable VolumeShaper.Configuration volumeShaperConfig) {
- try {
- return Ringtone.createV1WithCustomStreamType(context, streamType, ringtoneUri,
- volumeShaperConfig);
- } catch (Exception ex) {
- Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
- }
- return null;
- }
-
/**
* Gets a {@link Uri} for the ringtone at the given position in the {@link Cursor}.
*
@@ -783,13 +635,11 @@
*/
public static Uri getValidRingtoneUri(Context context) {
final RingtoneManager rm = new RingtoneManager(context);
-
- Uri uri = getValidRingtoneUriFromCursorAndClose(context,
- rm.queryMediaStore(/* internal= */ true));
+
+ Uri uri = getValidRingtoneUriFromCursorAndClose(context, rm.getInternalRingtones());
if (uri == null) {
- uri = getValidRingtoneUriFromCursorAndClose(context,
- rm.queryMediaStore(/* internal= */ false));
+ uri = getValidRingtoneUriFromCursorAndClose(context, rm.getMediaRingtones());
}
return uri;
@@ -810,26 +660,28 @@
}
}
- private Cursor queryMediaStore(boolean internal) {
- return queryMediaStore(mContext, internal);
+ @UnsupportedAppUsage
+ private Cursor getInternalRingtones() {
+ final Cursor res = query(
+ MediaStore.Audio.Media.INTERNAL_CONTENT_URI, INTERNAL_COLUMNS,
+ constructBooleanTrueWhereClause(mFilterColumns),
+ null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+ return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
}
- private Cursor queryMediaStore(Context context, boolean internal) {
- Uri contentUri = getContentUriForMedia(mMediaType, internal);
- String[] columns =
- mMediaType == Ringtone.MEDIA_VIBRATION ? MEDIA_VIBRATION_COLUMNS
- : MEDIA_AUDIO_COLUMNS;
- String whereClause = getWhereClauseForMedia(mMediaType, mFilterColumns);
- String sortOrder = getSortOrderForMedia(mMediaType);
+ private Cursor getMediaRingtones() {
+ final Cursor res = getMediaRingtones(mContext);
+ return new ExternalRingtonesCursorWrapper(res, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
+ }
- Cursor cursor = query(contentUri, columns, whereClause, /* selectionArgs= */ null,
- sortOrder, context);
-
- if (context.getUserId() != mContext.getUserId()) {
- contentUri = ContentProvider.maybeAddUserId(contentUri, context.getUserId());
- }
-
- return new ExternalRingtonesCursorWrapper(cursor, contentUri);
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private Cursor getMediaRingtones(Context context) {
+ // MediaStore now returns ringtones on other storage devices, even when
+ // we don't have storage or audio permissions
+ return query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS,
+ constructBooleanTrueWhereClause(mFilterColumns), null,
+ MediaStore.Audio.Media.DEFAULT_SORT_ORDER, context);
}
private void setFilterColumnsList(int type) {
@@ -848,56 +700,6 @@
columns.add(MediaStore.Audio.AudioColumns.IS_ALARM);
}
}
-
- /**
- * Returns the sort order for the specified media.
- *
- * @param media The RingtoneManager media type.
- * @return The sort order column.
- */
- private static String getSortOrderForMedia(@MediaType int media) {
- return media == Ringtone.MEDIA_VIBRATION ? MediaStore.Files.FileColumns.TITLE
- : MediaStore.Audio.Media.DEFAULT_SORT_ORDER;
- }
-
- /**
- * Returns the content URI based on the specified media and whether it's internal or external
- * storage.
- *
- * @param media The RingtoneManager media type.
- * @param internal Whether it's for internal or external storage.
- * @return The media content URI.
- */
- private static Uri getContentUriForMedia(@MediaType int media, boolean internal) {
- switch (media) {
- case Ringtone.MEDIA_VIBRATION:
- return MediaStore.Files.getContentUri(
- internal ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL);
- case Ringtone.MEDIA_SOUND:
- return internal ? MediaStore.Audio.Media.INTERNAL_CONTENT_URI
- : MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
- default:
- throw new IllegalArgumentException("Unsupported media type " + media);
- }
- }
-
- /**
- * Constructs a where clause based on the media type. This will be used to find all matching
- * sound or vibration files.
- *
- * @param media The RingtoneManager media type.
- * @param columns The columns that must be true, when media type is {@link Ringtone#MEDIA_SOUND}
- * @return The where clause.
- */
- private static String getWhereClauseForMedia(@MediaType int media, List<String> columns) {
- // TODO(b/296213309): Filtering by ringtone-type isn't supported yet for vibrations.
- if (media == Ringtone.MEDIA_VIBRATION) {
- return TextUtils.formatSimple("(%s='%s')", MediaStore.Files.FileColumns.MIME_TYPE,
- VibrationXmlParser.APPLICATION_VIBRATION_XML_MIME_TYPE);
- }
-
- return constructBooleanTrueWhereClause(columns);
- }
/**
* Constructs a where clause that consists of at least one column being 1
@@ -927,6 +729,14 @@
return sb.toString();
}
+
+ private Cursor query(Uri uri,
+ String[] projection,
+ String selection,
+ String[] selectionArgs,
+ String sortOrder) {
+ return query(uri, projection, selection, selectionArgs, sortOrder, mContext);
+ }
private Cursor query(Uri uri,
String[] projection,
@@ -954,14 +764,40 @@
* @return A {@link Ringtone} for the given URI, or null.
*/
public static Ringtone getRingtone(final Context context, Uri ringtoneUri) {
- if (Flags.hapticsCustomizationRingtoneV2Enabled()) {
- return new Ringtone.Builder(
- context, Ringtone.MEDIA_SOUND, getDefaultAudioAttributes(-1))
- .setUri(ringtoneUri)
- .build();
- } else {
- return createRingtoneV1WithStreamType(context, ringtoneUri, -1, null);
- }
+ // Don't set the stream type
+ return getRingtone(context, ringtoneUri, -1, true);
+ }
+
+ /**
+ * Returns a {@link Ringtone} with {@link VolumeShaper} if required for a given sound URI.
+ * <p>
+ * If the given URI cannot be opened for any reason, this method will
+ * attempt to fallback on another sound. If it cannot find any, it will
+ * return null.
+ *
+ * @param context A context used to query.
+ * @param ringtoneUri The {@link Uri} of a sound or ringtone.
+ * @param volumeShaperConfig config for volume shaper of the ringtone if applied.
+ * @return A {@link Ringtone} for the given URI, or null.
+ *
+ * @hide
+ */
+ public static Ringtone getRingtone(
+ final Context context, Uri ringtoneUri,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ // Don't set the stream type
+ return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig, true);
+ }
+
+ /**
+ * @hide
+ */
+ public static Ringtone getRingtone(final Context context, Uri ringtoneUri,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig,
+ boolean createLocalMediaPlayer) {
+ // Don't set the stream type
+ return getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig,
+ createLocalMediaPlayer);
}
/**
@@ -970,23 +806,64 @@
public static Ringtone getRingtone(final Context context, Uri ringtoneUri,
@Nullable VolumeShaper.Configuration volumeShaperConfig,
AudioAttributes audioAttributes) {
- // TODO: move caller(s) away from this method: inline the builder call.
- if (Flags.hapticsCustomizationRingtoneV2Enabled()) {
- return new Ringtone.Builder(context, Ringtone.MEDIA_SOUND, audioAttributes)
- .setUri(ringtoneUri)
- .setVolumeShaperConfig(volumeShaperConfig)
- .setUseExactAudioAttributes(true) // May be using audio-coupled via attrs
- .build();
- } else {
- try {
- return Ringtone.createV1WithCustomAudioAttributes(context, audioAttributes,
- ringtoneUri, volumeShaperConfig, /* allowRemote= */ true);
- } catch (Exception ex) {
- // Match broad catching of createRingtoneV1.
- Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
+ // Don't set the stream type
+ Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */,
+ volumeShaperConfig, false);
+ if (ringtone != null) {
+ ringtone.setAudioAttributesField(audioAttributes);
+ if (!ringtone.createLocalMediaPlayer()) {
+ Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
return null;
}
}
+ return ringtone;
+ }
+
+ //FIXME bypass the notion of stream types within the class
+ /**
+ * Returns a {@link Ringtone} for a given sound URI on the given stream
+ * type. Normally, if you change the stream type on the returned
+ * {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just
+ * an optimized route to avoid that.
+ *
+ * @param streamType The stream type for the ringtone, or -1 if it should
+ * not be set (and the default used instead).
+ * @param createLocalMediaPlayer when true, the ringtone returned will be fully
+ * created otherwise, it will require the caller to create the media player manually
+ * {@link Ringtone#createLocalMediaPlayer()} in order to play the Ringtone.
+ * @see #getRingtone(Context, Uri)
+ */
+ @UnsupportedAppUsage
+ private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType,
+ boolean createLocalMediaPlayer) {
+ return getRingtone(context, ringtoneUri, streamType, null /* volumeShaperConfig */,
+ createLocalMediaPlayer);
+ }
+
+ private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig,
+ boolean createLocalMediaPlayer) {
+ try {
+ final Ringtone r = new Ringtone(context, true);
+ if (streamType >= 0) {
+ //FIXME deprecated call
+ r.setStreamType(streamType);
+ }
+
+ r.setVolumeShaperConfig(volumeShaperConfig);
+ r.setUri(ringtoneUri, volumeShaperConfig);
+ if (createLocalMediaPlayer) {
+ if (!r.createLocalMediaPlayer()) {
+ Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
+ return null;
+ }
+ }
+ return r;
+ } catch (Exception ex) {
+ Log.e(TAG, "Failed to open ringtone " + ringtoneUri + ": " + ex);
+ }
+
+ return null;
}
/**
diff --git a/media/java/android/media/RingtoneV1.java b/media/java/android/media/RingtoneV1.java
deleted file mode 100644
index 3c54d4a..0000000
--- a/media/java/android/media/RingtoneV1.java
+++ /dev/null
@@ -1,614 +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 android.media;
-
-import android.annotation.Nullable;
-import android.compat.annotation.UnsupportedAppUsage;
-import android.content.Context;
-import android.content.res.AssetFileDescriptor;
-import android.content.res.Resources.NotFoundException;
-import android.media.audiofx.HapticGenerator;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.Build;
-import android.os.RemoteException;
-import android.os.Trace;
-import android.os.VibrationEffect;
-import android.provider.MediaStore;
-import android.provider.MediaStore.MediaColumns;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.IOException;
-import java.util.ArrayList;
-
-/**
- * Hosts original Ringtone implementation, retained for flagging large builder+vibration features
- * in RingtoneV2.java. This does not support new features in the V2 builder.
- *
- * Only modified methods are moved here.
- *
- * @hide
- */
-class RingtoneV1 implements Ringtone.ApiInterface {
- private static final String TAG = "RingtoneV1";
- private static final boolean LOGD = true;
-
- private static final String[] MEDIA_COLUMNS = new String[] {
- MediaStore.Audio.Media._ID,
- MediaStore.Audio.Media.TITLE
- };
- /** Selection that limits query results to just audio files */
- private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR "
- + MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')";
-
- // keep references on active Ringtones until stopped or completion listener called.
- private static final ArrayList<RingtoneV1> sActiveRingtones = new ArrayList<>();
-
- private final Context mContext;
- private final AudioManager mAudioManager;
- private VolumeShaper.Configuration mVolumeShaperConfig;
- private VolumeShaper mVolumeShaper;
-
- /**
- * Flag indicating if we're allowed to fall back to remote playback using
- * {@link #mRemotePlayer}. Typically this is false when we're the remote
- * player and there is nobody else to delegate to.
- */
- private final boolean mAllowRemote;
- private final IRingtonePlayer mRemotePlayer;
- private final Binder mRemoteToken;
-
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- private MediaPlayer mLocalPlayer;
- private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener();
- private HapticGenerator mHapticGenerator;
-
- @UnsupportedAppUsage
- private Uri mUri;
- private String mTitle;
-
- private AudioAttributes mAudioAttributes = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build();
- private boolean mPreferBuiltinDevice;
- // playback properties, use synchronized with mPlaybackSettingsLock
- private boolean mIsLooping = false;
- private float mVolume = 1.0f;
- private boolean mHapticGeneratorEnabled = false;
- private final Object mPlaybackSettingsLock = new Object();
-
- /** {@hide} */
- @UnsupportedAppUsage
- public RingtoneV1(Context context, boolean allowRemote) {
- mContext = context;
- mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
- mAllowRemote = allowRemote;
- mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
- mRemoteToken = allowRemote ? new Binder() : null;
- }
-
- /**
- * Sets the stream type where this ringtone will be played.
- *
- * @param streamType The stream, see {@link AudioManager}.
- * @deprecated use {@link #setAudioAttributes(AudioAttributes)}
- */
- @Deprecated
- public void setStreamType(int streamType) {
- PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", "setStreamType()");
- setAudioAttributes(new AudioAttributes.Builder()
- .setInternalLegacyStreamType(streamType)
- .build());
- }
-
- /**
- * Gets the stream type where this ringtone will be played.
- *
- * @return The stream type, see {@link AudioManager}.
- * @deprecated use of stream types is deprecated, see
- * {@link #setAudioAttributes(AudioAttributes)}
- */
- @Deprecated
- public int getStreamType() {
- return AudioAttributes.toLegacyStreamType(mAudioAttributes);
- }
-
- /**
- * Sets the {@link AudioAttributes} for this ringtone.
- * @param attributes the non-null attributes characterizing this ringtone.
- */
- public void setAudioAttributes(AudioAttributes attributes)
- throws IllegalArgumentException {
- setAudioAttributesField(attributes);
- // The audio attributes have to be set before the media player is prepared.
- // Re-initialize it.
- setUri(mUri, mVolumeShaperConfig);
- reinitializeActivePlayer();
- }
-
- /**
- * Same as {@link #setAudioAttributes(AudioAttributes)} except this one does not create
- * the media player.
- * @hide
- */
- public void setAudioAttributesField(@Nullable AudioAttributes attributes) {
- if (attributes == null) {
- throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
- }
- mAudioAttributes = attributes;
- }
-
- /**
- * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is
- * the one on which outgoing audio for SIM calls is played.
- *
- * @param audioManager the audio manage.
- * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if
- * none can be found.
- */
- private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) {
- AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
- for (AudioDeviceInfo device : deviceList) {
- if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
- return device;
- }
- }
- return null;
- }
-
- /**
- * Sets the preferred device of the ringtong playback to the built-in device.
- *
- * @hide
- */
- public boolean preferBuiltinDevice(boolean enable) {
- mPreferBuiltinDevice = enable;
- if (mLocalPlayer == null) {
- return true;
- }
- return mLocalPlayer.setPreferredDevice(getBuiltinDevice(mAudioManager));
- }
-
- /**
- * Creates a local media player for the ringtone using currently set attributes.
- * @return true if media player creation succeeded or is deferred,
- * false if it did not succeed and can't be tried remotely.
- * @hide
- */
- public boolean reinitializeActivePlayer() {
- Trace.beginSection("reinitializeActivePlayer");
- if (mUri == null) {
- Log.e(TAG, "Could not create media player as no URI was provided.");
- return mAllowRemote && mRemotePlayer != null;
- }
- destroyLocalPlayer();
- // try opening uri locally before delegating to remote player
- mLocalPlayer = new MediaPlayer();
- try {
- mLocalPlayer.setDataSource(mContext, mUri);
- mLocalPlayer.setAudioAttributes(mAudioAttributes);
- mLocalPlayer.setPreferredDevice(
- mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null);
- synchronized (mPlaybackSettingsLock) {
- applyPlaybackProperties_sync();
- }
- if (mVolumeShaperConfig != null) {
- mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
- }
- mLocalPlayer.prepare();
-
- } catch (SecurityException | IOException e) {
- destroyLocalPlayer();
- if (!mAllowRemote) {
- Log.w(TAG, "Remote playback not allowed: " + e);
- }
- }
-
- if (LOGD) {
- if (mLocalPlayer != null) {
- Log.d(TAG, "Successfully created local player");
- } else {
- Log.d(TAG, "Problem opening; delegating to remote player");
- }
- }
- Trace.endSection();
- return mLocalPlayer != null || (mAllowRemote && mRemotePlayer != null);
- }
-
- /**
- * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
- * If the ringtone has not been created, it will load based on URI provided at {@link #setUri}
- * and if not URI has been set, it will assume no haptic channels are present.
- * @hide
- */
- public boolean hasHapticChannels() {
- // FIXME: support remote player, or internalize haptic channels support and remove entirely.
- try {
- android.os.Trace.beginSection("Ringtone.hasHapticChannels");
- if (mLocalPlayer != null) {
- for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) {
- if (trackInfo.hasHapticChannels()) {
- return true;
- }
- }
- }
- } finally {
- android.os.Trace.endSection();
- }
- return false;
- }
-
- /**
- * Returns whether a local player has been created for this ringtone.
- * @hide
- */
- @VisibleForTesting
- public boolean hasLocalPlayer() {
- return mLocalPlayer != null;
- }
-
- public @Ringtone.RingtoneMedia int getEnabledMedia() {
- return Ringtone.MEDIA_SOUND; // RingtoneV2 only
- }
-
- public VibrationEffect getVibrationEffect() {
- return null; // RingtoneV2 only
- }
-
- /**
- * Returns the {@link AudioAttributes} used by this object.
- * @return the {@link AudioAttributes} that were set with
- * {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set.
- */
- public AudioAttributes getAudioAttributes() {
- return mAudioAttributes;
- }
-
- /**
- * Sets the player to be looping or non-looping.
- * @param looping whether to loop or not.
- */
- public void setLooping(boolean looping) {
- synchronized (mPlaybackSettingsLock) {
- mIsLooping = looping;
- applyPlaybackProperties_sync();
- }
- }
-
- /**
- * Returns whether the looping mode was enabled on this player.
- * @return true if this player loops when playing.
- */
- public boolean isLooping() {
- synchronized (mPlaybackSettingsLock) {
- return mIsLooping;
- }
- }
-
- /**
- * Sets the volume on this player.
- * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
- * corresponds to no attenuation being applied.
- */
- public void setVolume(float volume) {
- synchronized (mPlaybackSettingsLock) {
- if (volume < 0.0f) { volume = 0.0f; }
- if (volume > 1.0f) { volume = 1.0f; }
- mVolume = volume;
- applyPlaybackProperties_sync();
- }
- }
-
- /**
- * Returns the volume scalar set on this player.
- * @return a value between 0.0f and 1.0f.
- */
- public float getVolume() {
- synchronized (mPlaybackSettingsLock) {
- return mVolume;
- }
- }
-
- /**
- * Enable or disable the {@link android.media.audiofx.HapticGenerator} effect. The effect can
- * only be enabled on devices that support the effect.
- *
- * @return true if the HapticGenerator effect is successfully enabled. Otherwise, return false.
- * @see android.media.audiofx.HapticGenerator#isAvailable()
- */
- public boolean setHapticGeneratorEnabled(boolean enabled) {
- if (!HapticGenerator.isAvailable()) {
- return false;
- }
- synchronized (mPlaybackSettingsLock) {
- mHapticGeneratorEnabled = enabled;
- applyPlaybackProperties_sync();
- }
- return true;
- }
-
- /**
- * Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not.
- * @return true if the HapticGenerator is enabled.
- */
- public boolean isHapticGeneratorEnabled() {
- synchronized (mPlaybackSettingsLock) {
- return mHapticGeneratorEnabled;
- }
- }
-
- /**
- * Must be called synchronized on mPlaybackSettingsLock
- */
- private void applyPlaybackProperties_sync() {
- if (mLocalPlayer != null) {
- mLocalPlayer.setVolume(mVolume);
- mLocalPlayer.setLooping(mIsLooping);
- if (mHapticGenerator == null && mHapticGeneratorEnabled) {
- mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId());
- }
- if (mHapticGenerator != null) {
- mHapticGenerator.setEnabled(mHapticGeneratorEnabled);
- }
- } else if (mAllowRemote && (mRemotePlayer != null)) {
- try {
- mRemotePlayer.setPlaybackProperties(
- mRemoteToken, mVolume, mIsLooping, mHapticGeneratorEnabled);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting playback properties: ", e);
- }
- } else {
- Log.w(TAG,
- "Neither local nor remote player available when applying playback properties");
- }
- }
-
- /**
- * Returns a human-presentable title for ringtone. Looks in media
- * content provider. If not in either, uses the filename
- *
- * @param context A context used for querying.
- */
- public String getTitle(Context context) {
- if (mTitle != null) return mTitle;
- return mTitle = Ringtone.getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
- }
-
- /**
- * Set {@link Uri} to be used for ringtone playback.
- * {@link IRingtonePlayer}.
- *
- * @hide
- */
- @UnsupportedAppUsage
- public void setUri(Uri uri) {
- setUri(uri, null);
- }
-
- /**
- * @hide
- */
- public void setVolumeShaperConfig(@Nullable VolumeShaper.Configuration volumeShaperConfig) {
- mVolumeShaperConfig = volumeShaperConfig;
- }
-
- /**
- * Set {@link Uri} to be used for ringtone playback. Attempts to open
- * locally, otherwise will delegate playback to remote
- * {@link IRingtonePlayer}. Add {@link VolumeShaper} if required.
- *
- * @hide
- */
- public void setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig) {
- mVolumeShaperConfig = volumeShaperConfig;
- mUri = uri;
- if (mUri == null) {
- destroyLocalPlayer();
- }
- }
-
- /** {@hide} */
- @UnsupportedAppUsage
- public Uri getUri() {
- return mUri;
- }
-
- /**
- * Plays the ringtone.
- */
- public void play() {
- if (mLocalPlayer != null) {
- // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone
- // (typically because ringer mode is vibrate).
- if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
- != 0) {
- startLocalPlayer();
- } else if (!mAudioAttributes.areHapticChannelsMuted() && hasHapticChannels()) {
- // is haptic only ringtone
- startLocalPlayer();
- }
- } else if (mAllowRemote && (mRemotePlayer != null) && (mUri != null)) {
- final Uri canonicalUri = mUri.getCanonicalUri();
- final boolean looping;
- final float volume;
- synchronized (mPlaybackSettingsLock) {
- looping = mIsLooping;
- volume = mVolume;
- }
- try {
- mRemotePlayer.playWithVolumeShaping(mRemoteToken, canonicalUri, mAudioAttributes,
- volume, looping, mVolumeShaperConfig);
- } catch (RemoteException e) {
- if (!playFallbackRingtone()) {
- Log.w(TAG, "Problem playing ringtone: " + e);
- }
- }
- } else {
- if (!playFallbackRingtone()) {
- Log.w(TAG, "Neither local nor remote playback available");
- }
- }
- }
-
- /**
- * Stops a playing ringtone.
- */
- public void stop() {
- if (mLocalPlayer != null) {
- destroyLocalPlayer();
- } else if (mAllowRemote && (mRemotePlayer != null)) {
- try {
- mRemotePlayer.stop(mRemoteToken);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem stopping ringtone: " + e);
- }
- }
- }
-
- private void destroyLocalPlayer() {
- if (mLocalPlayer != null) {
- if (mHapticGenerator != null) {
- mHapticGenerator.release();
- mHapticGenerator = null;
- }
- mLocalPlayer.setOnCompletionListener(null);
- mLocalPlayer.reset();
- mLocalPlayer.release();
- mLocalPlayer = null;
- mVolumeShaper = null;
- synchronized (sActiveRingtones) {
- sActiveRingtones.remove(this);
- }
- }
- }
-
- private void startLocalPlayer() {
- if (mLocalPlayer == null) {
- return;
- }
- synchronized (sActiveRingtones) {
- sActiveRingtones.add(this);
- }
- if (LOGD) {
- Log.d(TAG, "Starting ringtone playback");
- }
- mLocalPlayer.setOnCompletionListener(mCompletionListener);
- mLocalPlayer.start();
- if (mVolumeShaper != null) {
- mVolumeShaper.apply(VolumeShaper.Operation.PLAY);
- }
- }
-
- /**
- * Whether this ringtone is currently playing.
- *
- * @return True if playing, false otherwise.
- */
- public boolean isPlaying() {
- if (mLocalPlayer != null) {
- return mLocalPlayer.isPlaying();
- } else if (mAllowRemote && (mRemotePlayer != null)) {
- try {
- return mRemotePlayer.isPlaying(mRemoteToken);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem checking ringtone: " + e);
- return false;
- }
- } else {
- Log.w(TAG, "Neither local nor remote playback available");
- return false;
- }
- }
-
- private boolean playFallbackRingtone() {
- int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes);
- if (mAudioManager.getStreamVolume(streamType) == 0) {
- return false;
- }
- int ringtoneType = RingtoneManager.getDefaultType(mUri);
- if (ringtoneType != -1 &&
- RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) {
- Log.w(TAG, "not playing fallback for " + mUri);
- return false;
- }
- // Default ringtone, try fallback ringtone.
- try {
- AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
- com.android.internal.R.raw.fallbackring);
- if (afd == null) {
- Log.e(TAG, "Could not load fallback ringtone");
- return false;
- }
- mLocalPlayer = new MediaPlayer();
- if (afd.getDeclaredLength() < 0) {
- mLocalPlayer.setDataSource(afd.getFileDescriptor());
- } else {
- mLocalPlayer.setDataSource(afd.getFileDescriptor(),
- afd.getStartOffset(),
- afd.getDeclaredLength());
- }
- mLocalPlayer.setAudioAttributes(mAudioAttributes);
- synchronized (mPlaybackSettingsLock) {
- applyPlaybackProperties_sync();
- }
- if (mVolumeShaperConfig != null) {
- mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
- }
- mLocalPlayer.prepare();
- startLocalPlayer();
- afd.close();
- } catch (IOException ioe) {
- destroyLocalPlayer();
- Log.e(TAG, "Failed to open fallback ringtone");
- return false;
- } catch (NotFoundException nfe) {
- Log.e(TAG, "Fallback ringtone does not exist");
- return false;
- }
- return true;
- }
-
- public boolean getPreferBuiltinDevice() {
- return mPreferBuiltinDevice;
- }
-
- public VolumeShaper.Configuration getVolumeShaperConfig() {
- return mVolumeShaperConfig;
- }
-
- public boolean isLocalOnly() {
- return mAllowRemote;
- }
-
- public boolean isUsingRemotePlayer() {
- // V2 testing api, but this is the v1 approximation.
- return (mLocalPlayer == null) && mAllowRemote && (mRemotePlayer != null);
- }
-
- class MyOnCompletionListener implements MediaPlayer.OnCompletionListener {
- @Override
- public void onCompletion(MediaPlayer mp) {
- synchronized (sActiveRingtones) {
- sActiveRingtones.remove(RingtoneV1.this);
- }
- mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
- }
- }
-}
diff --git a/media/java/android/media/RingtoneV2.java b/media/java/android/media/RingtoneV2.java
deleted file mode 100644
index f1a8155..0000000
--- a/media/java/android/media/RingtoneV2.java
+++ /dev/null
@@ -1,690 +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 android.media;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.compat.annotation.UnsupportedAppUsage;
-import android.content.Context;
-import android.content.res.AssetFileDescriptor;
-import android.content.res.Resources.NotFoundException;
-import android.media.Ringtone.Injectables;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.Trace;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.provider.MediaStore;
-import android.provider.MediaStore.MediaColumns;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.IOException;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * New Ringtone implementation, supporting vibration as well as sound, and configuration via a
- * builder. During flagged transition, the original implementation is in RingtoneV1.java.
- *
- * Only modified methods are moved here.
- *
- * @hide
- */
-class RingtoneV2 implements Ringtone.ApiInterface {
- private static final String TAG = "RingtoneV2";
-
- /**
- * The ringtone should only play sound. Any vibration is managed externally.
- * @hide
- */
- public static final int MEDIA_SOUND = 1;
- /**
- * The ringtone should only play vibration. Any sound is managed externally.
- * Requires the {@link android.Manifest.permission#VIBRATE} permission.
- * @hide
- */
- public static final int MEDIA_VIBRATION = 1 << 1;
- /**
- * The ringtone should play sound and vibration.
- * @hide
- */
- public static final int MEDIA_SOUND_AND_VIBRATION = MEDIA_SOUND | MEDIA_VIBRATION;
-
- // This is not a public value, because apps shouldn't enable "all" media - that wouldn't be
- // safe if new media types were added.
- static final int MEDIA_ALL = MEDIA_SOUND | MEDIA_VIBRATION;
-
- /**
- * Declares the types of media that this Ringtone is allowed to play.
- * @hide
- */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = "MEDIA_", value = {
- MEDIA_SOUND,
- MEDIA_VIBRATION,
- MEDIA_SOUND_AND_VIBRATION,
- })
- public @interface RingtoneMedia {}
-
- private static final String[] MEDIA_COLUMNS = new String[] {
- MediaStore.Audio.Media._ID,
- MediaStore.Audio.Media.TITLE
- };
- /** Selection that limits query results to just audio files */
- private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR "
- + MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')";
-
- private final Context mContext;
- private final Vibrator mVibrator;
- private final AudioManager mAudioManager;
- private VolumeShaper.Configuration mVolumeShaperConfig;
-
- /**
- * Flag indicating if we're allowed to fall back to remote playback using
- * {@link #mRemoteRingtoneService}. Typically this is false when we're the remote
- * player and there is nobody else to delegate to.
- */
- private final boolean mAllowRemote;
- private final IRingtonePlayer mRemoteRingtoneService;
- private final Injectables mInjectables;
-
- private final int mEnabledMedia;
-
- private final Uri mUri;
- private String mTitle;
-
- private AudioAttributes mAudioAttributes;
- private boolean mUseExactAudioAttributes;
- private boolean mPreferBuiltinDevice;
- private RingtonePlayer mActivePlayer;
- // playback properties, use synchronized with mPlaybackSettingsLock
- private boolean mIsLooping;
- private float mVolume;
- private boolean mHapticGeneratorEnabled;
- private final Object mPlaybackSettingsLock = new Object();
- private final VibrationEffect mVibrationEffect;
-
- /** Only for use by Ringtone constructor */
- RingtoneV2(@NonNull Context context, @NonNull Injectables injectables,
- boolean allowRemote, @Ringtone.RingtoneMedia int enabledMedia,
- @Nullable Uri uri, @NonNull AudioAttributes audioAttributes,
- boolean useExactAudioAttributes,
- @Nullable VolumeShaper.Configuration volumeShaperConfig,
- boolean preferBuiltinDevice, float soundVolume, boolean looping,
- boolean hapticGeneratorEnabled, @Nullable VibrationEffect vibrationEffect) {
- // Context
- mContext = context;
- mInjectables = injectables;
- mVibrator = mContext.getSystemService(Vibrator.class);
- mAudioManager = mContext.getSystemService(AudioManager.class);
- mRemoteRingtoneService = allowRemote ? mAudioManager.getRingtonePlayer() : null;
- mAllowRemote = (mRemoteRingtoneService != null); // Only set if allowed, and present.
-
- // Properties potentially propagated to remote player.
- mEnabledMedia = enabledMedia;
- mUri = uri;
- mAudioAttributes = audioAttributes;
- mUseExactAudioAttributes = useExactAudioAttributes;
- mVolumeShaperConfig = volumeShaperConfig;
- mPreferBuiltinDevice = preferBuiltinDevice; // system-only, not supported for remote play.
- mVolume = soundVolume;
- mIsLooping = looping;
- mHapticGeneratorEnabled = hapticGeneratorEnabled;
- mVibrationEffect = vibrationEffect;
- }
-
- /** @hide */
- @RingtoneMedia
- public int getEnabledMedia() {
- return mEnabledMedia;
- }
-
- /**
- * Sets the stream type where this ringtone will be played.
- *
- * @param streamType The stream, see {@link AudioManager}.
- * @deprecated use {@link #setAudioAttributes(AudioAttributes)}
- */
- @Deprecated
- public void setStreamType(int streamType) {
- setAudioAttributes(
- getAudioAttributesForLegacyStreamType(streamType, "setStreamType()"));
- }
-
- private AudioAttributes getAudioAttributesForLegacyStreamType(int streamType, String originOp) {
- PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", originOp);
- return new AudioAttributes.Builder()
- .setInternalLegacyStreamType(streamType)
- .build();
- }
-
- /**
- * Gets the stream type where this ringtone will be played.
- *
- * @return The stream type, see {@link AudioManager}.
- * @deprecated use of stream types is deprecated, see
- * {@link #setAudioAttributes(AudioAttributes)}
- */
- @Deprecated
- public int getStreamType() {
- return AudioAttributes.toLegacyStreamType(mAudioAttributes);
- }
-
- /**
- * Sets the {@link AudioAttributes} for this ringtone.
- * @param attributes the non-null attributes characterizing this ringtone.
- */
- public void setAudioAttributes(AudioAttributes attributes)
- throws IllegalArgumentException {
- // TODO: deprecate this method - it will be done with a builder.
- if (attributes == null) {
- throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
- }
- mAudioAttributes = attributes;
- // Setting the audio attributes requires re-initializing the player.
- if (mActivePlayer != null) {
- // The audio attributes have to be set before the media player is prepared.
- // Re-initialize it.
- reinitializeActivePlayer();
- }
- }
-
- /**
- * Returns the vibration effect that this ringtone was created with, if vibration is enabled.
- * Otherwise, returns null.
- * @hide
- */
- @Nullable
- public VibrationEffect getVibrationEffect() {
- return mVibrationEffect;
- }
-
- /** @hide */
- @VisibleForTesting
- public boolean getPreferBuiltinDevice() {
- return mPreferBuiltinDevice;
- }
-
- /** @hide */
- @VisibleForTesting
- public VolumeShaper.Configuration getVolumeShaperConfig() {
- return mVolumeShaperConfig;
- }
-
- /**
- * Returns whether this player is local only, or can defer to the remote player. The
- * result may differ from the builder if there is no remote player available at all.
- * @hide
- */
- @VisibleForTesting
- public boolean isLocalOnly() {
- return !mAllowRemote;
- }
-
- /** @hide */
- @VisibleForTesting
- public boolean isUsingRemotePlayer() {
- return mActivePlayer instanceof RemoteRingtonePlayer;
- }
-
- /**
- * Finds the output device of type {@link AudioDeviceInfo#TYPE_BUILTIN_SPEAKER}. This device is
- * the one on which outgoing audio for SIM calls is played.
- *
- * @param audioManager the audio manage.
- * @return the {@link AudioDeviceInfo} corresponding to the builtin device, or {@code null} if
- * none can be found.
- */
- private AudioDeviceInfo getBuiltinDevice(AudioManager audioManager) {
- AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
- for (AudioDeviceInfo device : deviceList) {
- if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
- return device;
- }
- }
- return null;
- }
-
- /**
- * Creates a local media player for the ringtone using currently set attributes.
- * @return true if media player creation succeeded or is deferred,
- * false if it did not succeed and can't be tried remotely.
- * @hide
- */
- public boolean reinitializeActivePlayer() {
- // Try creating a local media player, or fallback to creating a remote one.
- Trace.beginSection("reinitializeActivePlayer");
- try {
- if (mActivePlayer != null) {
- // This would only happen if calling the deprecated setAudioAttributes after
- // building the Ringtone.
- stopAndReleaseActivePlayer();
- }
-
- boolean vibrationOnly = (mEnabledMedia & MEDIA_ALL) == MEDIA_VIBRATION;
- // Vibration can come from the audio file if using haptic generator or if haptic
- // channels are a possibility.
- boolean maybeAudioVibration = mUri != null && mInjectables.isHapticPlaybackSupported()
- && (mHapticGeneratorEnabled || !mAudioAttributes.areHapticChannelsMuted());
-
- // VibrationEffect only, use the simplified player without checking for haptic channels.
- if (vibrationOnly && !maybeAudioVibration && mVibrationEffect != null) {
- mActivePlayer = new LocalRingtonePlayer.VibrationEffectPlayer(
- mVibrationEffect, mAudioAttributes, mVibrator, mIsLooping);
- return true;
- }
-
- AudioDeviceInfo preferredDevice =
- mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null;
- if (mUri != null) {
- mActivePlayer = LocalRingtonePlayer.create(mContext, mAudioManager, mVibrator, mUri,
- mAudioAttributes, vibrationOnly, mVibrationEffect, mInjectables,
- mVolumeShaperConfig, preferredDevice, mHapticGeneratorEnabled, mIsLooping,
- mVolume);
- } else {
- // Using the remote player won't help play a null Uri. Revert straight to fallback.
- // The vibration-only case was already covered above.
- mActivePlayer = createFallbackRingtonePlayer();
- // Fall through to attempting remote fallback play if null.
- }
-
- if (mActivePlayer == null && mAllowRemote) {
- mActivePlayer = new RemoteRingtonePlayer(mRemoteRingtoneService, mUri,
- mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect,
- mVolumeShaperConfig, mHapticGeneratorEnabled, mIsLooping, mVolume);
- }
-
- return mActivePlayer != null;
- } finally {
- if (mActivePlayer != null) {
- Log.d(TAG, "Initialized ringtone player with " + mActivePlayer.getClass());
- } else {
- Log.d(TAG, "Failed to initialize ringtone player");
- }
- Trace.endSection();
- }
- }
-
- @Nullable
- private LocalRingtonePlayer createFallbackRingtonePlayer() {
- int ringtoneType = RingtoneManager.getDefaultType(mUri);
- if (ringtoneType != -1
- && RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) {
- Log.w(TAG, "not playing fallback for " + mUri);
- return null;
- }
- // Default ringtone, try fallback ringtone.
- try (AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
- com.android.internal.R.raw.fallbackring)) {
- if (afd == null) {
- Log.e(TAG, "Could not load fallback ringtone");
- return null;
- }
-
- AudioDeviceInfo preferredDevice =
- mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null;
- return LocalRingtonePlayer.createForFallback(mAudioManager, mVibrator, afd,
- mAudioAttributes, mVibrationEffect, mInjectables, mVolumeShaperConfig,
- preferredDevice, mIsLooping, mVolume);
- } catch (NotFoundException nfe) {
- Log.e(TAG, "Fallback ringtone does not exist");
- return null;
- } catch (IOException e) {
- // As with the above messages, not including much information about the
- // failure so as not to expose details of the fallback ringtone resource.
- Log.e(TAG, "Exception reading fallback ringtone");
- return null;
- }
- }
-
- /**
- * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
- * @hide
- */
- public boolean hasHapticChannels() {
- return (mActivePlayer == null) ? false : mActivePlayer.hasHapticChannels();
- }
-
- /**
- * Returns the {@link AudioAttributes} used by this object.
- * @return the {@link AudioAttributes} that were set with
- * {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set.
- */
- public AudioAttributes getAudioAttributes() {
- return mAudioAttributes;
- }
-
- /**
- * Sets the player to be looping or non-looping.
- * @param looping whether to loop or not.
- */
- public void setLooping(boolean looping) {
- synchronized (mPlaybackSettingsLock) {
- mIsLooping = looping;
- if (mActivePlayer != null) {
- mActivePlayer.setLooping(looping);
- }
- }
- }
-
- /**
- * Returns whether the looping mode was enabled on this player.
- * @return true if this player loops when playing.
- */
- public boolean isLooping() {
- synchronized (mPlaybackSettingsLock) {
- return mIsLooping;
- }
- }
-
- /**
- * Sets the volume on this player.
- * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
- * corresponds to no attenuation being applied.
- */
- public void setVolume(float volume) {
- // Ignore if sound not enabled.
- if ((mEnabledMedia & MEDIA_SOUND) == 0) {
- return;
- }
- if (volume < 0.0f) {
- volume = 0.0f;
- } else if (volume > 1.0f) {
- volume = 1.0f;
- }
-
- synchronized (mPlaybackSettingsLock) {
- mVolume = volume;
- if (mActivePlayer != null) {
- mActivePlayer.setVolume(volume);
- }
- }
- }
-
- /**
- * Returns the volume scalar set on this player.
- * @return a value between 0.0f and 1.0f.
- */
- public float getVolume() {
- synchronized (mPlaybackSettingsLock) {
- return mVolume;
- }
- }
-
- /**
- * Enable or disable the {@link android.media.audiofx.HapticGenerator} effect. The effect can
- * only be enabled on devices that support the effect.
- *
- * @return true if the HapticGenerator effect is successfully enabled. Otherwise, return false.
- * @see android.media.audiofx.HapticGenerator#isAvailable()
- */
- public boolean setHapticGeneratorEnabled(boolean enabled) {
- if (!mInjectables.isHapticGeneratorAvailable()) {
- return false;
- }
- synchronized (mPlaybackSettingsLock) {
- mHapticGeneratorEnabled = enabled;
- if (mActivePlayer != null) {
- mActivePlayer.setHapticGeneratorEnabled(enabled);
- }
- }
- return true;
- }
-
- /**
- * Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not.
- * @return true if the HapticGenerator is enabled.
- */
- public boolean isHapticGeneratorEnabled() {
- synchronized (mPlaybackSettingsLock) {
- return mHapticGeneratorEnabled;
- }
- }
-
- /**
- * Returns a human-presentable title for ringtone. Looks in media
- * content provider. If not in either, uses the filename
- *
- * @param context A context used for querying.
- */
- public String getTitle(Context context) {
- if (mTitle != null) return mTitle;
- return mTitle = Ringtone.getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
- }
-
-
- /** {@hide} */
- @UnsupportedAppUsage
- public Uri getUri() {
- return mUri;
- }
-
- /**
- * Plays the ringtone.
- */
- public void play() {
- if (mActivePlayer != null) {
- Log.d(TAG, "Starting ringtone playback");
- if (mActivePlayer.play()) {
- return;
- } else {
- // Discard active player: play() is only meant to be called once.
- stopAndReleaseActivePlayer();
- }
- }
- if (!playFallbackRingtone()) {
- Log.w(TAG, "Neither local nor remote playback available");
- }
- }
-
- /**
- * Stops a playing ringtone.
- */
- public void stop() {
- stopAndReleaseActivePlayer();
- }
-
- private void stopAndReleaseActivePlayer() {
- if (mActivePlayer != null) {
- mActivePlayer.stopAndRelease();
- mActivePlayer = null;
- }
- }
-
- /**
- * Whether this ringtone is currently playing.
- *
- * @return True if playing, false otherwise.
- */
- public boolean isPlaying() {
- if (mActivePlayer != null) {
- return mActivePlayer.isPlaying();
- } else {
- Log.w(TAG, "No active ringtone player");
- return false;
- }
- }
-
- /**
- * Fallback during the play stage rather than initialization, typically due to an issue
- * communicating with the remote player.
- */
- private boolean playFallbackRingtone() {
- if (mActivePlayer != null) {
- Log.wtf(TAG, "Playing fallback ringtone with another active player");
- stopAndReleaseActivePlayer();
- }
- int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes);
- if (mAudioManager.getStreamVolume(streamType) == 0) {
- // TODO: Return true? If volume is off, this is a successful play.
- return false;
- }
- mActivePlayer = createFallbackRingtonePlayer();
- if (mActivePlayer == null) {
- return false; // the create method logs if it returns null.
- } else if (mActivePlayer.play()) {
- return true;
- } else {
- stopAndReleaseActivePlayer();
- return false;
- }
- }
-
- void setTitle(String title) {
- mTitle = title;
- }
-
- /**
- * Play a specific ringtone. This interface is implemented by either local (this process) or
- * proxied-remote playback via AudioManager.getRingtonePlayer, so that the caller
- * (Ringtone class) can just use a single player after the initial creation.
- * @hide
- */
- interface RingtonePlayer {
- /**
- * Start playing the ringtone, returning false if there was a problem that
- * requires falling back to the fallback ringtone resource.
- */
- boolean play();
- boolean isPlaying();
- void stopAndRelease();
-
- // Mutating playback methods.
- void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo);
- void setLooping(boolean looping);
- void setHapticGeneratorEnabled(boolean enabled);
- void setVolume(float volume);
-
- boolean hasHapticChannels();
- }
-
- /**
- * Remote RingtonePlayer. All operations are delegated via the IRingtonePlayer interface, which
- * should ultimately be backed by a RingtoneLocalPlayer within the system services.
- */
- static class RemoteRingtonePlayer implements RingtonePlayer {
- private final IBinder mRemoteToken = new Binder();
- private final IRingtonePlayer mRemoteRingtoneService;
- private final Uri mCanonicalUri;
- private final int mEnabledMedia;
- private final VibrationEffect mVibrationEffect;
- private final VolumeShaper.Configuration mVolumeShaperConfig;
- private final AudioAttributes mAudioAttributes;
- private final boolean mUseExactAudioAttributes;
- private boolean mIsLooping;
- private float mVolume;
- private boolean mHapticGeneratorEnabled;
-
- RemoteRingtonePlayer(@NonNull IRingtonePlayer remoteRingtoneService,
- @NonNull Uri uri, @NonNull AudioAttributes audioAttributes,
- boolean useExactAudioAttributes,
- @RingtoneMedia int enabledMedia, @Nullable VibrationEffect vibrationEffect,
- @Nullable VolumeShaper.Configuration volumeShaperConfig,
- boolean hapticGeneratorEnabled, boolean initialIsLooping, float initialVolume) {
- mRemoteRingtoneService = remoteRingtoneService;
- mCanonicalUri = (uri == null) ? null : uri.getCanonicalUri();
- mAudioAttributes = audioAttributes;
- mUseExactAudioAttributes = useExactAudioAttributes;
- mEnabledMedia = enabledMedia;
- mVibrationEffect = vibrationEffect;
- mVolumeShaperConfig = volumeShaperConfig;
- mHapticGeneratorEnabled = hapticGeneratorEnabled;
- mIsLooping = initialIsLooping;
- mVolume = initialVolume;
- }
-
- @Override
- public boolean play() {
- try {
- mRemoteRingtoneService.playRemoteRingtone(mRemoteToken, mCanonicalUri,
- mAudioAttributes, mUseExactAudioAttributes, mEnabledMedia, mVibrationEffect,
- mVolume, mIsLooping, mHapticGeneratorEnabled, mVolumeShaperConfig);
- return true;
- } catch (RemoteException e) {
- Log.w(TAG, "Problem playing ringtone: " + e);
- return false;
- }
- }
-
- @Override
- public boolean isPlaying() {
- try {
- return mRemoteRingtoneService.isPlaying(mRemoteToken);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem checking ringtone isPlaying: " + e);
- return false;
- }
- }
-
- @Override
- public void stopAndRelease() {
- try {
- mRemoteRingtoneService.stop(mRemoteToken);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem stopping ringtone: " + e);
- }
- }
-
- @Override
- public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
- // un-implemented for remote (but not used outside system).
- }
-
- @Override
- public void setLooping(boolean looping) {
- mIsLooping = looping;
- try {
- mRemoteRingtoneService.setLooping(mRemoteToken, looping);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting looping: " + e);
- }
- }
-
- @Override
- public void setHapticGeneratorEnabled(boolean enabled) {
- mHapticGeneratorEnabled = enabled;
- try {
- mRemoteRingtoneService.setHapticGeneratorEnabled(mRemoteToken, enabled);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting hapticGeneratorEnabled: " + e);
- }
- }
-
- @Override
- public void setVolume(float volume) {
- mVolume = volume;
- try {
- mRemoteRingtoneService.setVolume(mRemoteToken, volume);
- } catch (RemoteException e) {
- Log.w(TAG, "Problem setting volume: " + e);
- }
- }
-
- @Override
- public boolean hasHapticChannels() {
- // FIXME: support remote player, or internalize haptic channels support and remove
- // entirely.
- return false;
- }
- }
-
-}
diff --git a/media/java/android/media/browse/MediaBrowserUtils.java b/media/java/android/media/browse/MediaBrowserUtils.java
index 19d9f00..8c008bc 100644
--- a/media/java/android/media/browse/MediaBrowserUtils.java
+++ b/media/java/android/media/browse/MediaBrowserUtils.java
@@ -18,6 +18,9 @@
import android.os.Bundle;
+import java.util.Collections;
+import java.util.List;
+
/**
* @hide
*/
@@ -75,4 +78,29 @@
}
return false;
}
+
+ /**
+ * Returns a paged version of the given {@code list}, using the paging parameters in {@code
+ * options}.
+ */
+ public static List<MediaBrowser.MediaItem> applyPagingOptions(
+ List<MediaBrowser.MediaItem> list, final Bundle options) {
+ if (list == null) {
+ return null;
+ }
+ int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1);
+ int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1);
+ if (page == -1 && pageSize == -1) {
+ return list;
+ }
+ int fromIndex = pageSize * page;
+ int toIndex = fromIndex + pageSize;
+ if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
+ return Collections.EMPTY_LIST;
+ }
+ if (toIndex > list.size()) {
+ toIndex = list.size();
+ }
+ return list.subList(fromIndex, toIndex);
+ }
}
diff --git a/media/java/android/media/tv/BroadcastInfoRequest.java b/media/java/android/media/tv/BroadcastInfoRequest.java
index 694756c..eb980a3 100644
--- a/media/java/android/media/tv/BroadcastInfoRequest.java
+++ b/media/java/android/media/tv/BroadcastInfoRequest.java
@@ -16,9 +16,11 @@
package android.media.tv;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
+import android.media.tv.flags.Flags;
import android.os.Parcel;
import android.os.Parcelable;
@@ -51,14 +53,14 @@
/**
* Request option: one-way
* <p> With this option, no response is expected after sending the request.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public static final int REQUEST_OPTION_ONEWAY = 2;
/**
* Request option: one-shot
* <p> With this option, only one response will be given per request.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public static final int REQUEST_OPTION_ONESHOT = 3;
public static final @NonNull Parcelable.Creator<BroadcastInfoRequest> CREATOR =
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 432e109..6658918 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -768,12 +768,12 @@
/**
* Informs the application that the video freeze state has been updated.
*
- * When {@code true}, the video is frozen on the last frame but audio playback remains
+ * <p>When {@code true}, the video is frozen on the last frame but audio playback remains
* active.
*
* @param isFrozen Whether or not the video is frozen
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void notifyVideoFreezeUpdated(boolean isFrozen) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java
index ffc121e..e604cb7 100644
--- a/media/java/android/media/tv/TvView.java
+++ b/media/java/android/media/tv/TvView.java
@@ -683,13 +683,15 @@
* Sets whether or not the video is frozen. While the video is frozen, audio playback will
* continue.
*
- * <p> This should be invoked after a {@link TvInteractiveAppService.Session#requestCommand} is
+ * <p>This should be invoked after a {@link TvInteractiveAppService.Session#requestCommand} is
* received with the command to freeze the video.
*
- * <p> This will freeze the video to the last frame when the state is set to {@code true}.
+ * <p>This will freeze the video to the last frame when the state is set to {@code true}.
+ *
+ * @see TvView.TvInputCallback#setVideoFrozen(boolean)
* @param isFrozen whether or not the video is frozen.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void setVideoFrozen(boolean isFrozen) {
if (mSession != null) {
mSession.setVideoFrozen(isFrozen);
@@ -1325,6 +1327,16 @@
public void onTvMessage(@NonNull String inputId,
@TvInputManager.TvMessageType int type, @NonNull Bundle data) {
}
+
+ /**
+ * This is called when the video freeze status is updated.
+ *
+ * @see #setVideoFrozen(boolean)
+ * @param inputId The ID of the TV input bound to this view.
+ * @param isFrozen Whether or not the video is currently frozen on the las
+ */
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void onVideoFreezeUpdated(@NonNull String inputId, boolean isFrozen) {}
}
/**
@@ -1753,5 +1765,19 @@
mCallback.onTvMessage(mInputId, type, data);
}
}
+
+ @Override
+ public void onVideoFreezeUpdated(Session session, boolean isFrozen) {
+ if (DEBUG) {
+ Log.d(TAG, "onVideoFreezeUpdated(isFrozen=" + isFrozen + ")");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onVideoFreezeUpdated - session not created");
+ return;
+ }
+ if (mCallback != null) {
+ mCallback.onVideoFreezeUpdated(mInputId, isFrozen);
+ }
+ }
}
}
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
index 15ce9fd..59b10c6 100644
--- a/media/java/android/media/tv/ad/TvAdManager.java
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -68,7 +68,6 @@
* <p>Type: String
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String APP_LINK_KEY_PACKAGE_NAME = "package_name";
@@ -77,7 +76,6 @@
* <p>Type: String
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String APP_LINK_KEY_CLASS_NAME = "class_name";
@@ -86,7 +84,6 @@
* <p>Type: String
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String APP_LINK_KEY_COMMAND_TYPE = "command_type";
@@ -95,7 +92,6 @@
* <p>Type: String
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String APP_LINK_KEY_SERVICE_ID = "service_id";
@@ -104,7 +100,6 @@
* <p>Type: String
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String APP_LINK_KEY_BACK_URI = "back_uri";
@@ -112,7 +107,6 @@
* Broadcast intent action to send app command to TV app.
*
* @see #sendAppLinkCommand(String, Bundle)
- * @hide
*/
public static final String ACTION_APP_LINK_COMMAND =
"android.media.tv.ad.action.APP_LINK_COMMAND";
@@ -123,7 +117,6 @@
*
* @see #sendAppLinkCommand(String, Bundle)
* @see #ACTION_APP_LINK_COMMAND
- * @hide
*/
public static final String INTENT_KEY_TV_INPUT_ID = "tv_input_id";
@@ -134,7 +127,6 @@
* @see #sendAppLinkCommand(String, Bundle)
* @see #ACTION_APP_LINK_COMMAND
* @see TvAdServiceInfo#getId()
- * @hide
*/
public static final String INTENT_KEY_AD_SERVICE_ID = "ad_service_id";
@@ -144,7 +136,6 @@
*
* @see #sendAppLinkCommand(String, Bundle)
* @see #ACTION_APP_LINK_COMMAND
- * @hide
*/
public static final String INTENT_KEY_CHANNEL_URI = "channel_uri";
@@ -155,7 +146,6 @@
*
* @see #sendAppLinkCommand(String, Bundle)
* @see #ACTION_APP_LINK_COMMAND
- * @hide
*/
public static final String INTENT_KEY_COMMAND_TYPE = "command_type";
@@ -486,8 +476,17 @@
*
* @param serviceId The ID of TV AD service which the command to be sent to. The ID can be found
* in {@link TvAdServiceInfo#getId()}.
- * @param command The command to be sent.
- * @hide
+ * @param command The command to be sent. The command is a bundle with the following keys:
+ * <ul>
+ * <li>{@link #APP_LINK_KEY_PACKAGE_NAME}: The package name of the app to be
+ * launched.
+ * <li>{@link #APP_LINK_KEY_CLASS_NAME}: The class name of the app to be
+ * launched.
+ * <li>{@link #APP_LINK_KEY_COMMAND_TYPE}: The command type.
+ * <li>{@link #APP_LINK_KEY_SERVICE_ID}: The ID of the TV AD service.
+ * <li>{@link #APP_LINK_KEY_BACK_URI}: The URI to be used to return to the
+ * previous app.
+ * </ul>
*/
public void sendAppLinkCommand(@NonNull String serviceId, @NonNull Bundle command) {
try {
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
index d7b29d3..6c8a8fd 100644
--- a/media/java/android/media/tv/ad/TvAdService.java
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -136,7 +136,6 @@
* Called when app link command is received.
*
* @see TvAdManager#sendAppLinkCommand(String, Bundle)
- * @hide
*/
public void onAppLinkCommand(@NonNull Bundle command) {
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index 498eec6..7cf32ec 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -2634,8 +2634,8 @@
/**
* This is called when
- * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, int, byte[])} is
- * called.
+ * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, int, byte[])}
+ * is called.
*
* @param session A {@link TvInteractiveAppService.Session} associated with this callback.
* @param signingId the ID to identify the request.
@@ -2644,7 +2644,6 @@
* @param host The host of the SSL CLient Authentication Server
* @param port The port of the SSL Client Authentication Server
* @param data the original bytes to be signed.
- * @hide
*/
public void onRequestSigning(
Session session, String signingId, String algorithm, String host,
@@ -2657,7 +2656,6 @@
* @param session A {@link TvInteractiveAppService.Session} associated with this callback.
* @param host the host name of the SSL authentication server.
* @param port the port of the SSL authentication server. E.g., 443
- * @hide
*/
public void onRequestCertificate(Session session, String host, int port) {
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 6b0620c..eba26d4 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -742,8 +742,8 @@
* @param host the host name of the SSL authentication server.
* @param port the port of the SSL authentication server. E.g., 443
* @param cert the SSL certificate received.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void onCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) {
}
@@ -896,13 +896,13 @@
}
/**
- * Called when video becomes frozen or unfrozen. Audio playback will continue while
- * video will be frozen to the last frame if {@code true}.
+ * Called when video becomes frozen or unfrozen. Audio playback will continue while video
+ * will be frozen to the last frame if {@code true}.
+ *
* @param isFrozen Whether or not the video is frozen.
- * @hide
*/
- public void onVideoFreezeUpdated(boolean isFrozen) {
- }
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void onVideoFreezeUpdated(boolean isFrozen) {}
/**
* Called when content is allowed.
@@ -1666,9 +1666,9 @@
* @see #onSigningResult(String, byte[])
* @see TvInteractiveAppView#createBiInteractiveApp(Uri, Bundle)
* @see TvInteractiveAppView#BI_INTERACTIVE_APP_KEY_ALIAS
- * @hide
*/
@CallSuper
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
@NonNull String host, int port, @NonNull byte[] data) {
executeOrPostRunnableOnMainThread(new Runnable() {
@@ -1695,8 +1695,9 @@
*
* @param host the host name of the SSL authentication server.
* @param port the port of the SSL authentication server. E.g., 443
- * @hide
*/
+ @CallSuper
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void requestCertificate(@NonNull String host, int port) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 584ea84..29a3b98 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -723,12 +723,12 @@
}
/**
- * Alerts the TV Interactive app that the video freeze state has been updated.
- * If {@code true}, the video is frozen on the last frame while audio playback continues.
+ * Alerts the TV Interactive app that the video freeze state has been updated. If {@code true},
+ * the video is frozen on the last frame while audio playback continues.
*
* @param isFrozen Whether the video is frozen.
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void notifyVideoFreezeUpdated(boolean isFrozen) {
if (DEBUG) {
Log.d(TAG, "notifyVideoFreezeUpdated");
@@ -760,12 +760,12 @@
}
/**
- * Send the requested SSL certificate to the TV Interactive App
+ * Sends the requested SSL certificate to the TV Interactive App
* @param host the host name of the SSL authentication server.
* @param port the port of the SSL authentication server. E.g., 443
* @param cert the SSL certificate requested
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
public void sendCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) {
if (DEBUG) {
Log.d(TAG, "sendCertificate");
@@ -1390,6 +1390,37 @@
}
/**
+ * This is called when
+ * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, int, byte[])}
+ * is called.
+ *
+ * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+ * @param signingId the ID to identify the request.
+ * @param algorithm the standard name of the signature algorithm requested, such as
+ * MD5withRSA, SHA256withDSA, etc.
+ * @param host The hostname of the SSL authentication server.
+ * @param port The port of the SSL authentication server.
+ * @param data the original bytes to be signed.
+ */
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void onRequestSigning(@NonNull String iAppServiceId, @NonNull String signingId,
+ @NonNull String algorithm, @NonNull String host, int port, @NonNull byte[] data) {
+ }
+
+ /**
+ * This is called when
+ * {@link TvInteractiveAppService.Session#requestCertificate(String, int)} is called.
+ *
+ * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+ * @param host The hostname of the SSL authentication server.
+ * @param port The port of the SSL authentication server.
+ */
+ @FlaggedApi(Flags.FLAG_TIAF_V_APIS)
+ public void onRequestCertificate(@NonNull String iAppServiceId, @NonNull String host,
+ int port) {
+ }
+
+ /**
* This is called when {@link TvInteractiveAppService.Session#setTvRecordingInfo(String,
* TvRecordingInfo)} is called.
*
@@ -1957,5 +1988,34 @@
mCallback.onRequestSigning(mIAppServiceId, id, algorithm, alias, data);
}
}
+
+ @Override
+ public void onRequestSigning(
+ Session session, String id, String algorithm, String host, int port, byte[] data) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestSigning");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestSigning - session not created");
+ return;
+ }
+ if (mCallback != null && Flags.tiafVApis()) {
+ mCallback.onRequestSigning(mIAppServiceId, id, algorithm, host, port, data);
+ }
+ }
+
+ @Override
+ public void onRequestCertificate(Session session, String host, int port) {
+ if (DEBUG) {
+ Log.d(TAG, "onRequestCertificate");
+ }
+ if (this != mSessionCallback) {
+ Log.w(TAG, "onRequestCertificate - session not created");
+ return;
+ }
+ if (mCallback != null && Flags.tiafVApis()) {
+ mCallback.onRequestCertificate(mIAppServiceId, host, port);
+ }
+ }
}
}
diff --git a/media/java/android/service/media/MediaBrowserService.java b/media/java/android/service/media/MediaBrowserService.java
index e8ef464..ba7ab9a 100644
--- a/media/java/android/service/media/MediaBrowserService.java
+++ b/media/java/android/service/media/MediaBrowserService.java
@@ -48,7 +48,6 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -728,7 +727,7 @@
List<MediaBrowser.MediaItem> filteredList =
(flag & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
- ? applyOptions(list, options) : list;
+ ? MediaBrowserUtils.applyPagingOptions(list, options) : list;
final ParceledListSlice<MediaBrowser.MediaItem> pls;
if (filteredList == null) {
pls = null;
@@ -762,27 +761,6 @@
}
}
- private List<MediaBrowser.MediaItem> applyOptions(List<MediaBrowser.MediaItem> list,
- final Bundle options) {
- if (list == null) {
- return null;
- }
- int page = options.getInt(MediaBrowser.EXTRA_PAGE, -1);
- int pageSize = options.getInt(MediaBrowser.EXTRA_PAGE_SIZE, -1);
- if (page == -1 && pageSize == -1) {
- return list;
- }
- int fromIndex = pageSize * page;
- int toIndex = fromIndex + pageSize;
- if (page < 0 || pageSize < 1 || fromIndex >= list.size()) {
- return Collections.EMPTY_LIST;
- }
- if (toIndex > list.size()) {
- toIndex = list.size();
- }
- return list.subList(fromIndex, toIndex);
- }
-
private void performLoadItem(String itemId, final ConnectionRecord connection,
final ResultReceiver receiver) {
final Result<MediaBrowser.MediaItem> result =
diff --git a/media/tests/MediaFrameworkTest/Android.bp b/media/tests/MediaFrameworkTest/Android.bp
index 7a329bc..1325fc1 100644
--- a/media/tests/MediaFrameworkTest/Android.bp
+++ b/media/tests/MediaFrameworkTest/Android.bp
@@ -22,7 +22,6 @@
"android-ex-camera2",
"android.media.playback.flags-aconfig-java",
"flag-junit",
- "testables",
"testng",
"truth",
],
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/OWNERS b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/OWNERS
deleted file mode 100644
index 6d5f82c..0000000
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# Haptics team also works on Ringtone
-per-file *Ringtone* = file:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java
deleted file mode 100644
index 3c0c684..0000000
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java
+++ /dev/null
@@ -1,840 +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.mediaframeworktest.unit;
-
-import static android.media.Ringtone.MEDIA_SOUND;
-import static android.media.Ringtone.MEDIA_SOUND_AND_VIBRATION;
-import static android.media.Ringtone.MEDIA_VIBRATION;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.doCallRealMethod;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.AssetFileDescriptor;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.IRingtonePlayer;
-import android.media.MediaPlayer;
-import android.media.Ringtone;
-import android.media.audiofx.HapticGenerator;
-import android.net.Uri;
-import android.os.IBinder;
-import android.os.VibrationAttributes;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.testing.TestableContext;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.mediaframeworktest.R;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runner.RunWith;
-import org.junit.runners.model.Statement;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.io.FileNotFoundException;
-import java.util.ArrayDeque;
-import java.util.Map;
-import java.util.Queue;
-
-@RunWith(AndroidJUnit4.class)
-public class RingtoneTest {
-
- private static final Uri SOUND_URI = Uri.parse("content://fake-sound-uri");
-
- private static final AudioAttributes RINGTONE_ATTRIBUTES =
- audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
- private static final AudioAttributes RINGTONE_ATTRIBUTES_WITH_HC =
- new AudioAttributes.Builder(RINGTONE_ATTRIBUTES).setHapticChannelsMuted(false).build();
- private static final VibrationAttributes RINGTONE_VIB_ATTRIBUTES =
- new VibrationAttributes.Builder(RINGTONE_ATTRIBUTES).build();
-
- private static final VibrationEffect VIBRATION_EFFECT =
- VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100}, -1);
- private static final VibrationEffect VIBRATION_EFFECT_REPEATING =
- VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100, 50}, 1);
-
- @Rule
- public final RingtoneInjectablesTrackingTestRule
- mMediaPlayerRule = new RingtoneInjectablesTrackingTestRule();
-
- @Captor private ArgumentCaptor<IBinder> mIBinderCaptor;
- @Mock private IRingtonePlayer mMockRemotePlayer;
- @Mock private Vibrator mMockVibrator;
- private AudioManager mSpyAudioManager;
- private TestableContext mContext;
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- TestableContext testContext =
- new TestableContext(InstrumentationRegistry.getTargetContext(), null);
- testContext.getTestablePermissions().setPermission(Manifest.permission.VIBRATE,
- PackageManager.PERMISSION_GRANTED);
- AudioManager realAudioManager = testContext.getSystemService(AudioManager.class);
- mSpyAudioManager = spy(realAudioManager);
- when(mSpyAudioManager.getRingtonePlayer()).thenReturn(mMockRemotePlayer);
- testContext.addMockSystemService(AudioManager.class, mSpyAudioManager);
- testContext.addMockSystemService(Vibrator.class, mMockVibrator);
-
- mContext = spy(testContext);
- }
-
- @Test
- public void testRingtone_fullLifecycleUsingLocalMediaPlayer() throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES).setUri(SOUND_URI).build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES);
- assertThat(ringtone.getVolume()).isEqualTo(1.0f);
- assertThat(ringtone.isLooping()).isEqualTo(false);
- assertThat(ringtone.isHapticGeneratorEnabled()).isEqualTo(false);
- assertThat(ringtone.getPreferBuiltinDevice()).isFalse();
- assertThat(ringtone.getVolumeShaperConfig()).isNull();
- assertThat(ringtone.isLocalOnly()).isFalse();
-
- // Prepare
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES);
- verify(mockMediaPlayer).setVolume(1.0f);
- verify(mockMediaPlayer).setLooping(false);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- verifyLocalPlay(mockMediaPlayer);
-
- // Verify dynamic controls.
- ringtone.setVolume(0.8f);
- verify(mockMediaPlayer).setVolume(0.8f);
- when(mockMediaPlayer.isLooping()).thenReturn(false);
- ringtone.setLooping(true);
- verify(mockMediaPlayer).isLooping();
- verify(mockMediaPlayer).setLooping(true);
- HapticGenerator mockHapticGenerator =
- mMediaPlayerRule.expectHapticGenerator(mockMediaPlayer);
- ringtone.setHapticGeneratorEnabled(true);
- verify(mockHapticGenerator).setEnabled(true);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
-
- // This test is intended to strictly verify all interactions with MediaPlayer in a local
- // playback case. This shouldn't be necessary in other tests that have the same basic
- // setup.
- verifyNoMoreInteractions(mockMediaPlayer);
- verify(mockHapticGenerator).release();
- verifyNoMoreInteractions(mockHapticGenerator);
- verifyZeroInteractions(mMockRemotePlayer);
- verifyZeroInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaPlayerWithAudioCoupledOverride() throws Exception {
- // Audio coupled playback is enabled in the incoming attributes, plus an instruction
- // to leave the attributes alone. This test verifies that the attributes reach the
- // media player without changing.
- final AudioAttributes audioAttributes = RINGTONE_ATTRIBUTES_WITH_HC;
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true);
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND, audioAttributes)
- .setUri(SOUND_URI)
- .setUseExactAudioAttributes(true)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getAudioAttributes()).isEqualTo(audioAttributes);
-
- // Prepare
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, audioAttributes);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- verifyLocalPlay(mockMediaPlayer);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
-
- verifyZeroInteractions(mMockRemotePlayer);
- verifyZeroInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_fullLifecycleUsingRemoteMediaPlayer() throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- setupFileNotFound(mockMediaPlayer, SOUND_URI);
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isTrue();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getAudioAttributes()).isEqualTo(RINGTONE_ATTRIBUTES);
- assertThat(ringtone.getVolume()).isEqualTo(1.0f);
- assertThat(ringtone.isLooping()).isEqualTo(false);
- assertThat(ringtone.isHapticGeneratorEnabled()).isEqualTo(false);
- assertThat(ringtone.getPreferBuiltinDevice()).isFalse();
- assertThat(ringtone.getVolumeShaperConfig()).isNull();
- assertThat(ringtone.isLocalOnly()).isFalse();
-
- // Initialization did try to create a local media player.
- verify(mockMediaPlayer).setDataSource(mContext, SOUND_URI);
- // setDataSource throws file not found, so nothing else will happen on the local player.
- verify(mockMediaPlayer).release();
-
- // Delegates to remote media player.
- ringtone.play();
- verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), eq(SOUND_URI),
- eq(RINGTONE_ATTRIBUTES), eq(false), eq(MEDIA_SOUND), isNull(),
- eq(1.0f), eq(false), eq(false), isNull());
- IBinder remoteToken = mIBinderCaptor.getValue();
-
- // Verify dynamic controls.
- ringtone.setVolume(0.8f);
- verify(mMockRemotePlayer).setVolume(remoteToken, 0.8f);
- ringtone.setLooping(true);
- verify(mMockRemotePlayer).setLooping(remoteToken, true);
- ringtone.setHapticGeneratorEnabled(true);
- verify(mMockRemotePlayer).setHapticGeneratorEnabled(remoteToken, true);
-
- ringtone.stop();
- verify(mMockRemotePlayer).stop(remoteToken);
- verifyNoMoreInteractions(mMockRemotePlayer);
- verifyNoMoreInteractions(mockMediaPlayer);
- verifyZeroInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaWithVibration() throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Prepare
- // Uses attributes with haptic channels enabled, but will use the effect when there aren't
- // any present.
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
- verify(mockMediaPlayer).setVolume(1.0f);
- verify(mockMediaPlayer).setLooping(false);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
-
- verifyLocalPlay(mockMediaPlayer);
- verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
-
- // Verify dynamic controls.
- ringtone.setVolume(0.8f);
- verify(mockMediaPlayer).setVolume(0.8f);
-
- // Set looping doesn't affect an already-started vibration.
- when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original
- ringtone.setLooping(true);
- verify(mockMediaPlayer).isLooping();
- verify(mockMediaPlayer).setLooping(true);
-
- // This is ignored because there's a vibration effect being used.
- ringtone.setHapticGeneratorEnabled(true);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
- verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
-
- // This test is intended to strictly verify all interactions with MediaPlayer in a local
- // playback case. This shouldn't be necessary in other tests that have the same basic
- // setup.
- verifyNoMoreInteractions(mockMediaPlayer);
- verifyZeroInteractions(mMockRemotePlayer);
- verifyNoMoreInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaWithVibrationOnly() throws Exception {
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- Ringtone ringtone =
- newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES)
- // TODO: set sound uri too in diff test
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION);
- assertThat(ringtone.getUri()).isNull();
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Play
- ringtone.play();
-
- verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
-
- // Verify dynamic controls (no-op without sound)
- ringtone.setVolume(0.8f);
-
- // Set looping doesn't affect an already-started vibration.
- ringtone.setLooping(true);
-
- // This is ignored because there's a vibration effect being used and no sound.
- ringtone.setHapticGeneratorEnabled(true);
-
- // Release
- ringtone.stop();
- verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
-
- // This test is intended to strictly verify all interactions with MediaPlayer in a local
- // playback case. This shouldn't be necessary in other tests that have the same basic
- // setup.
- verifyZeroInteractions(mMockRemotePlayer);
- verifyNoMoreInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaWithVibrationOnlyAndSoundUriNoHapticChannels()
- throws Exception {
- // A media player will still be created for vibration-only because the vibration can come
- // from haptic channels on the sound file (although in this case it doesn't).
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false);
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- Ringtone ringtone =
- newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Prepare
- // Uses attributes with haptic channels enabled, but will abandon the MediaPlayer when it
- // knows there aren't any.
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
- verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted.
- verify(mockMediaPlayer).setLooping(false);
- verify(mockMediaPlayer).prepare();
- verify(mockMediaPlayer).release(); // abandoned: no haptic channels.
-
- // Play
- ringtone.play();
-
- verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
-
- // Verify dynamic controls (no-op without sound)
- ringtone.setVolume(0.8f);
-
- // Set looping doesn't affect an already-started vibration.
- ringtone.setLooping(true);
-
- // This is ignored because there's a vibration effect being used and no sound.
- ringtone.setHapticGeneratorEnabled(true);
-
- // Release
- ringtone.stop();
- verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
-
- // This test is intended to strictly verify all interactions with MediaPlayer in a local
- // playback case. This shouldn't be necessary in other tests that have the same basic
- // setup.
- verifyZeroInteractions(mMockRemotePlayer);
- verifyNoMoreInteractions(mMockVibrator);
- verifyNoMoreInteractions(mockMediaPlayer);
- }
-
- @Test
- public void testRingtone_localMediaWithVibrationOnlyAndSoundUriWithHapticChannels()
- throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true);
- Ringtone ringtone =
- newBuilder(MEDIA_VIBRATION, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_VIBRATION);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Prepare
- // Uses attributes with haptic channels enabled, but will use the effect when there aren't
- // any present.
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
- verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted.
- verify(mockMediaPlayer).setLooping(false);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- // Vibrator.vibrate isn't called because the vibration comes from the sound.
- verifyLocalPlay(mockMediaPlayer);
-
- // Verify dynamic controls (no-op without sound)
- ringtone.setVolume(0.8f);
-
- when(mockMediaPlayer.isLooping()).thenReturn(false); // Checks original
- ringtone.setLooping(true);
- verify(mockMediaPlayer).isLooping();
- verify(mockMediaPlayer).setLooping(true);
-
- // This is ignored because it's using haptic channels.
- ringtone.setHapticGeneratorEnabled(true);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
-
- // This test is intended to strictly verify all interactions with MediaPlayer in a local
- // playback case. This shouldn't be necessary in other tests that have the same basic
- // setup.
- verifyZeroInteractions(mMockRemotePlayer);
- verifyZeroInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaWithVibrationPrefersHapticChannels() throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, true);
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Prepare
- // The attributes here have haptic channels enabled (unlike above)
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- when(mockMediaPlayer.isPlaying()).thenReturn(true);
- verifyLocalPlay(mockMediaPlayer);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
-
- verifyZeroInteractions(mMockRemotePlayer);
- // Nothing after the initial hasVibrator - it uses audio-coupled.
- verifyNoMoreInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_localMediaWithVibrationButSoundMuted() throws Exception {
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- mMediaPlayerRule.setHasHapticChannels(mockMediaPlayer, false);
- doReturn(0).when(mSpyAudioManager)
- .getStreamVolume(AudioAttributes.toLegacyStreamType(RINGTONE_ATTRIBUTES));
- when(mMockVibrator.hasVibrator()).thenReturn(true);
- Ringtone ringtone =
- newBuilder(MEDIA_SOUND_AND_VIBRATION, RINGTONE_ATTRIBUTES)
- .setUri(SOUND_URI)
- .setVibrationEffect(VIBRATION_EFFECT)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
- verify(mMockVibrator).hasVibrator();
-
- // Verify all the properties.
- assertThat(ringtone.getEnabledMedia()).isEqualTo(MEDIA_SOUND_AND_VIBRATION);
- assertThat(ringtone.getUri()).isEqualTo(SOUND_URI);
- assertThat(ringtone.getVibrationEffect()).isEqualTo(VIBRATION_EFFECT);
-
- // Prepare
- // The attributes here have haptic channels enabled (unlike above)
- verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- // The media player is never played, because sound is muted.
- verify(mockMediaPlayer, never()).start();
- when(mockMediaPlayer.isPlaying()).thenReturn(true);
- verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES);
-
- // Release
- ringtone.stop();
- verify(mockMediaPlayer).release();
- verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE);
-
- verifyZeroInteractions(mMockRemotePlayer);
- // Nothing after the initial hasVibrator - it uses audio-coupled.
- verifyNoMoreInteractions(mMockVibrator);
- }
-
- @Test
- public void testRingtone_nullMediaOnBuilderUsesFallback() throws Exception {
- AssetFileDescriptor testResourceFd =
- mContext.getResources().openRawResourceFd(R.raw.shortmp3);
- // Ensure it will flow as expected.
- assertThat(testResourceFd).isNotNull();
- assertThat(testResourceFd.getDeclaredLength()).isAtLeast(0);
- mContext.getOrCreateTestableResources()
- .addOverride(com.android.internal.R.raw.fallbackring, testResourceFd);
-
- MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer();
- Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
- .setUri(null)
- .build();
- assertThat(ringtone).isNotNull();
- assertThat(ringtone.isUsingRemotePlayer()).isFalse();
-
- // Delegates straight to fallback in local player.
- // Prepare
- verifyLocalPlayerFallbackSetup(mockMediaPlayer, testResourceFd, RINGTONE_ATTRIBUTES);
- verify(mockMediaPlayer).setVolume(1.0f);
- verify(mockMediaPlayer).setLooping(false);
- verify(mockMediaPlayer).prepare();
-
- // Play
- ringtone.play();
- verifyLocalPlay(mockMediaPlayer);
-
- // Release
- ringtone.stop();
- verifyLocalStop(mockMediaPlayer);
-
- verifyNoMoreInteractions(mockMediaPlayer);
- verifyNoMoreInteractions(mMockRemotePlayer);
- }
-
- @Test
- public void testRingtone_nullMediaOnBuilderUsesFallbackViaRemote() throws Exception {
- mContext.getOrCreateTestableResources()
- .addOverride(com.android.internal.R.raw.fallbackring, null);
- Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
- .setUri(null)
- .setLooping(true) // distinct from haptic generator, to match plumbing
- .build();
- assertThat(ringtone).isNotNull();
- // Local player fallback fails as the resource isn't found (no media player creation is
- // attempted), and then goes on to create the remote player.
- assertThat(ringtone.isUsingRemotePlayer()).isTrue();
-
- ringtone.play();
- verify(mMockRemotePlayer).playRemoteRingtone(mIBinderCaptor.capture(), isNull(),
- eq(RINGTONE_ATTRIBUTES), eq(false),
- eq(MEDIA_SOUND), isNull(),
- eq(1.0f), eq(true), eq(false), isNull());
- ringtone.stop();
- verify(mMockRemotePlayer).stop(mIBinderCaptor.getValue());
- verifyNoMoreInteractions(mMockRemotePlayer);
- }
-
- @Test
- public void testRingtone_noMediaSetOnBuilderFallbackFailsAndNoRemote() throws Exception {
- mContext.getOrCreateTestableResources()
- .addOverride(com.android.internal.R.raw.fallbackring, null);
- Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES)
- .setUri(null)
- .setLocalOnly()
- .build();
- // Local player fallback fails as the resource isn't found (no media player creation is
- // attempted), and since there is no local player, the ringtone ends up having nothing to
- // do.
- assertThat(ringtone).isNull();
- }
-
- private Ringtone.Builder newBuilder(@Ringtone.RingtoneMedia int ringtoneMedia,
- AudioAttributes audioAttributes) {
- return new Ringtone.Builder(mContext, ringtoneMedia, audioAttributes)
- .setInjectables(mMediaPlayerRule.injectables);
- }
-
- private static AudioAttributes audioAttributes(int audioUsage) {
- return new AudioAttributes.Builder()
- .setUsage(audioUsage)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build();
- }
-
- /** Makes the mock get some sort of file access problem. */
- private void setupFileNotFound(MediaPlayer mockMediaPlayer, Uri uri) throws Exception {
- doThrow(new FileNotFoundException("Fake file not found"))
- .when(mockMediaPlayer).setDataSource(any(Context.class), eq(uri));
- }
-
- private void verifyLocalPlayerSetup(MediaPlayer mockPlayer, Uri expectedUri,
- AudioAttributes expectedAudioAttributes) throws Exception {
- verify(mockPlayer).setDataSource(mContext, expectedUri);
- verify(mockPlayer).setAudioAttributes(expectedAudioAttributes);
- verify(mockPlayer).setPreferredDevice(null);
- verify(mockPlayer).prepare();
- }
-
- private void verifyLocalPlayerFallbackSetup(MediaPlayer mockPlayer, AssetFileDescriptor afd,
- AudioAttributes expectedAudioAttributes) throws Exception {
- // This is very specific but it's a simple way to test that the test resource matches.
- if (afd.getDeclaredLength() < 0) {
- verify(mockPlayer).setDataSource(afd.getFileDescriptor());
- } else {
- verify(mockPlayer).setDataSource(afd.getFileDescriptor(),
- afd.getStartOffset(),
- afd.getDeclaredLength());
- }
- verify(mockPlayer).setAudioAttributes(expectedAudioAttributes);
- verify(mockPlayer).setPreferredDevice(null);
- verify(mockPlayer).prepare();
- }
-
- private void verifyLocalPlay(MediaPlayer mockMediaPlayer) {
- verify(mockMediaPlayer).setOnCompletionListener(any());
- verify(mockMediaPlayer).start();
- }
-
- private void verifyLocalStop(MediaPlayer mockMediaPlayer) {
- verify(mockMediaPlayer).stop();
- verify(mockMediaPlayer).setOnCompletionListener(isNull());
- verify(mockMediaPlayer).reset();
- verify(mockMediaPlayer).release();
- }
-
- /**
- * This rule ensures that all expected media player creations from the factory do actually
- * occur. The reason for this level of control is that creating a media player is fairly
- * expensive and blocking, so we do want unit tests of this class to "declare" interactions
- * of all created media players.
- *
- * This needs to be a TestRule so that the teardown assertions can be skipped if the test has
- * failed (and media player assertions may just be a distracting side effect). Otherwise, the
- * teardown failures hide the real test ones.
- */
- public static class RingtoneInjectablesTrackingTestRule implements TestRule {
- public Ringtone.Injectables injectables = new TestInjectables();
- public boolean hapticGeneratorAvailable = true;
-
- // Queue of (local) media players, in order of expected creation. Enqueue using
- // expectNewMediaPlayer(), dequeued by the media player factory passed to Ringtone.
- // This queue is asserted to be empty at the end of the test.
- private Queue<MediaPlayer> mMockMediaPlayerQueue = new ArrayDeque<>();
-
- // Similar to media players, but for haptic generator, which also needs releasing.
- private Map<MediaPlayer, HapticGenerator> mMockHapticGeneratorMap = new ArrayMap<>();
-
- // Media players with haptic channels.
- private ArraySet<MediaPlayer> mHapticChannels = new ArraySet<>();
-
- @Override
- public Statement apply(Statement base, Description description) {
- return new Statement() {
- @Override
- public void evaluate() throws Throwable {
- base.evaluate();
- // Only assert if the test didn't fail (base.evaluate() would throw).
- assertWithMessage("Test setup an expectLocalMediaPlayer but it wasn't consumed")
- .that(mMockMediaPlayerQueue).isEmpty();
- // Only assert if the test didn't fail (base.evaluate() would throw).
- assertWithMessage(
- "Test setup an expectLocalHapticGenerator but it wasn't consumed")
- .that(mMockHapticGeneratorMap).isEmpty();
- }
- };
- }
-
- private TestMediaPlayer expectLocalMediaPlayer() {
- TestMediaPlayer mockMediaPlayer = Mockito.mock(TestMediaPlayer.class);
- // Delegate to simulated methods. This means they can be verified but also reflect
- // realistic transitions from the TestMediaPlayer.
- doCallRealMethod().when(mockMediaPlayer).start();
- doCallRealMethod().when(mockMediaPlayer).stop();
- doCallRealMethod().when(mockMediaPlayer).setLooping(anyBoolean());
- when(mockMediaPlayer.isLooping()).thenCallRealMethod();
- when(mockMediaPlayer.isLooping()).thenCallRealMethod();
- mMockMediaPlayerQueue.add(mockMediaPlayer);
- return mockMediaPlayer;
- }
-
- private HapticGenerator expectHapticGenerator(MediaPlayer mockMediaPlayer) {
- HapticGenerator mockHapticGenerator = Mockito.mock(HapticGenerator.class);
- // A test should never want this.
- assertWithMessage("Can't expect a second haptic generator created "
- + "for one media player")
- .that(mMockHapticGeneratorMap.put(mockMediaPlayer, mockHapticGenerator))
- .isNull();
- return mockHapticGenerator;
- }
-
- private void setHasHapticChannels(MediaPlayer mp, boolean hasHapticChannels) {
- if (hasHapticChannels) {
- mHapticChannels.add(mp);
- } else {
- mHapticChannels.remove(mp);
- }
- }
-
- private class TestInjectables extends Ringtone.Injectables {
- @Override
- public MediaPlayer newMediaPlayer() {
- assertWithMessage(
- "Unexpected MediaPlayer creation. Bug or need expectNewMediaPlayer")
- .that(mMockMediaPlayerQueue)
- .isNotEmpty();
- return mMockMediaPlayerQueue.remove();
- }
-
- @Override
- public boolean isHapticGeneratorAvailable() {
- return hapticGeneratorAvailable;
- }
-
- @Override
- public HapticGenerator createHapticGenerator(MediaPlayer mediaPlayer) {
- HapticGenerator mockHapticGenerator = mMockHapticGeneratorMap.remove(mediaPlayer);
- assertWithMessage("Unexpected HapticGenerator creation. "
- + "Bug or need expectHapticGenerator")
- .that(mockHapticGenerator)
- .isNotNull();
- return mockHapticGenerator;
- }
-
- @Override
- public boolean isHapticPlaybackSupported() {
- return true;
- }
-
- @Override
- public boolean hasHapticChannels(MediaPlayer mp) {
- return mHapticChannels.contains(mp);
- }
- }
- }
-
- /**
- * MediaPlayer relies on a native backend and so its necessary to intercept calls from
- * fake usage hitting them.
- *
- * Mocks don't work directly on native calls, but if they're overridden then it does work.
- * Some basic state faking is also done to make the mocks more realistic.
- */
- private static class TestMediaPlayer extends MediaPlayer {
- private boolean mIsPlaying = false;
- private boolean mIsLooping = false;
-
- @Override
- public void start() {
- mIsPlaying = true;
- }
-
- @Override
- public void stop() {
- mIsPlaying = false;
- }
-
- @Override
- public void setLooping(boolean value) {
- mIsLooping = value;
- }
-
- @Override
- public boolean isLooping() {
- return mIsLooping;
- }
-
- @Override
- public boolean isPlaying() {
- return mIsPlaying;
- }
-
- void simulatePlayingFinished() {
- if (!mIsPlaying) {
- throw new IllegalStateException(
- "Attempted to pretend playing finished when not playing");
- }
- mIsPlaying = false;
- }
- }
-}
diff --git a/media/tests/ringtone/Android.bp b/media/tests/ringtone/Android.bp
deleted file mode 100644
index 55b98c4..0000000
--- a/media/tests/ringtone/Android.bp
+++ /dev/null
@@ -1,30 +0,0 @@
-package {
- // See: http://go/android-license-faq
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_test {
- name: "MediaRingtoneTests",
-
- srcs: ["src/**/*.java"],
-
- libs: [
- "android.test.runner",
- "android.test.base",
- ],
-
- static_libs: [
- "androidx.test.rules",
- "testng",
- "androidx.test.ext.truth",
- "frameworks-base-testutils",
- ],
-
- test_suites: [
- "device-tests",
- "automotive-tests",
- ],
-
- platform_apis: true,
- certificate: "platform",
-}
diff --git a/media/tests/ringtone/AndroidManifest.xml b/media/tests/ringtone/AndroidManifest.xml
deleted file mode 100644
index 27eda07..0000000
--- a/media/tests/ringtone/AndroidManifest.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.framework.base.media.ringtone.tests">
-
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.MANAGE_USERS" />
-
- <application android:debuggable="true">
- <uses-library android:name="android.test.runner" />
-
- <activity android:name="MediaRingtoneTests"
- android:label="Media Ringtone Tests"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN"/>
- <category android:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
-
- </application>
-
- <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.framework.base.media.ringtone.tests"
- android:label="Media Ringtone Tests"/>
-</manifest>
diff --git a/media/tests/ringtone/TEST_MAPPING b/media/tests/ringtone/TEST_MAPPING
deleted file mode 100644
index 6f25c14..0000000
--- a/media/tests/ringtone/TEST_MAPPING
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "presubmit": [
- {
- "name": "MediaRingtoneTests",
- "options": [
- {"exclude-annotation": "androidx.test.filters.LargeTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"},
- {"exclude-annotation": "org.junit.Ignore"}
- ]
- }
- ],
- "postsubmit": [
- {
- "name": "MediaRingtoneTests",
- "options": [
- {"exclude-annotation": "org.junit.Ignore"}
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/media/tests/ringtone/res/raw/test_haptic_file.ahv b/media/tests/ringtone/res/raw/test_haptic_file.ahv
deleted file mode 100644
index d6eba1a..0000000
--- a/media/tests/ringtone/res/raw/test_haptic_file.ahv
+++ /dev/null
@@ -1,17 +0,0 @@
-<vibration-effect>
- <waveform-effect>
- <waveform-entry durationMs="63" amplitude="255"/>
- <waveform-entry durationMs="63" amplitude="231"/>
- <waveform-entry durationMs="63" amplitude="208"/>
- <waveform-entry durationMs="63" amplitude="185"/>
- <waveform-entry durationMs="63" amplitude="162"/>
- <waveform-entry durationMs="63" amplitude="139"/>
- <waveform-entry durationMs="63" amplitude="115"/>
- <waveform-entry durationMs="63" amplitude="92"/>
- <waveform-entry durationMs="63" amplitude="69"/>
- <waveform-entry durationMs="63" amplitude="46"/>
- <waveform-entry durationMs="63" amplitude="23"/>
- <waveform-entry durationMs="63" amplitude="0"/>
- <waveform-entry durationMs="1250" amplitude="0"/>
- </waveform-effect>
-</vibration-effect>
diff --git a/media/tests/ringtone/res/raw/test_sound_file.mp3 b/media/tests/ringtone/res/raw/test_sound_file.mp3
deleted file mode 100644
index c1b2fdf..0000000
--- a/media/tests/ringtone/res/raw/test_sound_file.mp3
+++ /dev/null
Binary files differ
diff --git a/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java b/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java
deleted file mode 100644
index a92b298..0000000
--- a/media/tests/ringtone/src/com/android/media/RingtoneManagerTest.java
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * 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 com.android.media;
-
-import static com.google.android.mms.ContentType.AUDIO_MP3;
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeTrue;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Environment;
-import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
-import android.os.vibrator.persistence.VibrationXmlParser;
-import android.provider.MediaStore;
-import android.text.TextUtils;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.framework.base.media.ringtone.tests.R;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-@RunWith(Parameterized.class)
-public class RingtoneManagerTest {
- @RingtoneManager.MediaType
- private final int mMediaType;
- private final List<Uri> mAddedFilesUri;
- private Context mContext;
- private RingtoneManager mRingtoneManager;
- private long mTimestamp;
-
- @Parameterized.Parameters(name = "media = {0}")
- public static Iterable<?> data() {
- return Arrays.asList(Ringtone.MEDIA_SOUND, Ringtone.MEDIA_VIBRATION);
- }
-
- public RingtoneManagerTest(@RingtoneManager.MediaType int mediaType) {
- mMediaType = mediaType;
- mAddedFilesUri = new ArrayList<>();
- }
-
- @Before
- public void setUp() throws Exception {
- mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
- mTimestamp = SystemClock.uptimeMillis();
- mRingtoneManager = new RingtoneManager(mContext);
- mRingtoneManager.setMediaType(mMediaType);
- }
-
- @After
- public void tearDown() {
- // Clean up media store
- for (Uri fileUri : mAddedFilesUri) {
- mContext.getContentResolver().delete(fileUri, null);
- }
- }
-
- @Test
- public void testSetMediaType_withValidValue_setsMediaCorrectly() {
- mRingtoneManager.setMediaType(mMediaType);
- assertThat(mRingtoneManager.getMediaType()).isEqualTo(mMediaType);
- }
-
- @Test
- public void testSetMediaType_withInvalidValue_throwsException() {
- assertThrows(IllegalArgumentException.class, () -> mRingtoneManager.setMediaType(999));
- }
-
- @Test
- public void testSetMediaType_afterCallingGetCursor_throwsException() {
- mRingtoneManager.getCursor();
- assertThrows(IllegalStateException.class, () -> mRingtoneManager.setMediaType(mMediaType));
- }
-
- @Test
- public void testGetRingtone_ringtoneHasCorrectTitle() throws Exception {
- String fileName = generateUniqueFileName("new_file");
- Ringtone ringtone = addNewRingtoneToMediaStore(mRingtoneManager, fileName);
-
- assertThat(ringtone.getTitle(mContext)).isEqualTo(fileName);
- }
-
- @Test
- public void testGetRingtone_ringtoneCanBePlayedAndStopped() throws Exception {
- //TODO(b/261571543) Remove this assumption once we support playing vibrations.
- assumeTrue(mMediaType == Ringtone.MEDIA_SOUND);
- String fileName = generateUniqueFileName("new_file");
- Ringtone ringtone = addNewRingtoneToMediaStore(mRingtoneManager, fileName);
-
- ringtone.play();
- assertThat(ringtone.isPlaying()).isTrue();
-
- ringtone.stop();
- assertThat(ringtone.isPlaying()).isFalse();
- }
-
- @Test
- public void testGetCursor_withDifferentMedia_returnsCorrectCursor() throws Exception {
- RingtoneManager audioRingtoneManager = new RingtoneManager(mContext);
- String audioFileName = generateUniqueFileName("ringtone");
- addNewRingtoneToMediaStore(audioRingtoneManager, audioFileName);
-
- RingtoneManager vibrationRingtoneManager = new RingtoneManager(mContext);
- vibrationRingtoneManager.setMediaType(Ringtone.MEDIA_VIBRATION);
- String vibrationFileName = generateUniqueFileName("vibration");
- addNewRingtoneToMediaStore(vibrationRingtoneManager, vibrationFileName);
-
- Cursor audioCursor = audioRingtoneManager.getCursor();
- Cursor vibrationCursor = vibrationRingtoneManager.getCursor();
-
- List<String> audioTitles = extractRecordTitles(audioCursor);
- List<String> vibrationTitles = extractRecordTitles(vibrationCursor);
-
- assertThat(audioTitles).contains(audioFileName);
- assertThat(audioTitles).doesNotContain(vibrationFileName);
-
- assertThat(vibrationTitles).contains(vibrationFileName);
- assertThat(vibrationTitles).doesNotContain(audioFileName);
- }
-
- private List<String> extractRecordTitles(Cursor cursor) {
- List<String> titles = new ArrayList<>();
-
- if (cursor.moveToFirst()) {
- do {
- String title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX);
- titles.add(title);
- } while (cursor.moveToNext());
- }
-
- return titles;
- }
-
- private Ringtone addNewRingtoneToMediaStore(RingtoneManager ringtoneManager, String fileName)
- throws Exception {
- Uri fileUri = ringtoneManager.getMediaType() == Ringtone.MEDIA_SOUND ? addAudioFile(
- fileName) : addVibrationFile(fileName);
- mAddedFilesUri.add(fileUri);
-
- int ringtonePosition = ringtoneManager.getRingtonePosition(fileUri);
- Ringtone ringtone = ringtoneManager.getRingtone(ringtonePosition);
- // Validate this is the expected ringtone.
- assertThat(ringtone.getUri()).isEqualTo(fileUri);
- return ringtone;
- }
-
- private Uri addAudioFile(String fileName) throws Exception {
- ContentResolver resolver = mContext.getContentResolver();
- ContentValues contentValues = new ContentValues();
- contentValues.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName + ".mp3");
- contentValues.put(MediaStore.Audio.Media.RELATIVE_PATH, Environment.DIRECTORY_RINGTONES);
- contentValues.put(MediaStore.Audio.Media.MIME_TYPE, AUDIO_MP3);
- contentValues.put(MediaStore.Audio.Media.TITLE, fileName);
- contentValues.put(MediaStore.Audio.Media.IS_RINGTONE, 1);
-
- Uri contentUri = resolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
- contentValues);
- writeRawDataToFile(resolver, contentUri, R.raw.test_sound_file);
-
- return resolver.canonicalizeOrElse(contentUri);
- }
-
- private Uri addVibrationFile(String fileName) throws Exception {
- ContentResolver resolver = mContext.getContentResolver();
- ContentValues contentValues = new ContentValues();
- contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName + ".ahv");
- contentValues.put(MediaStore.Files.FileColumns.RELATIVE_PATH,
- Environment.DIRECTORY_DOWNLOADS);
- contentValues.put(MediaStore.Files.FileColumns.MIME_TYPE,
- VibrationXmlParser.APPLICATION_VIBRATION_XML_MIME_TYPE);
- contentValues.put(MediaStore.Files.FileColumns.TITLE, fileName);
-
- Uri contentUri = resolver.insert(MediaStore.Files.getContentUri(MediaStore
- .VOLUME_EXTERNAL), contentValues);
- writeRawDataToFile(resolver, contentUri, R.raw.test_haptic_file);
-
- return resolver.canonicalizeOrElse(contentUri);
- }
-
- private void writeRawDataToFile(ContentResolver resolver, Uri contentUri, int rawResource)
- throws Exception {
- try (ParcelFileDescriptor pfd =
- resolver.openFileDescriptor(contentUri, "w", null)) {
- InputStream inputStream = mContext.getResources().openRawResource(rawResource);
- FileOutputStream outputStream = new FileOutputStream(pfd.getFileDescriptor());
- outputStream.write(inputStream.readAllBytes());
-
- inputStream.close();
- outputStream.flush();
- outputStream.close();
-
- } catch (Exception e) {
- throw new Exception("Failed to write data to file", e);
- }
- }
-
- private String generateUniqueFileName(String prefix) {
- return TextUtils.formatSimple("%s_%d", prefix, mTimestamp);
- }
-
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 0ccb07a..b13df61 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -58,6 +58,7 @@
private val providerEnabledList: List<ProviderData>
private val providerDisabledList: List<DisabledProviderData>?
val resultReceiver: ResultReceiver?
+ val finalResponseReceiver: ResultReceiver?
var initialUiState: UiState
@@ -105,6 +106,11 @@
ResultReceiver::class.java
)
+ finalResponseReceiver = intent.getParcelableExtra(
+ Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+ ResultReceiver::class.java
+ )
+
isReqForAllOptions = intent.getBooleanExtra(
Constants.EXTRA_REQ_FOR_ALL_OPTIONS,
/*defaultValue=*/ false
@@ -200,7 +206,7 @@
}
fun onCancel(cancelCode: Int) {
- sendCancellationCode(cancelCode, requestInfo?.token, resultReceiver)
+ sendCancellationCode(cancelCode, requestInfo?.token, resultReceiver, finalResponseReceiver)
}
fun onOptionSelected(
@@ -219,6 +225,10 @@
)
val resultDataBundle = Bundle()
UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultDataBundle)
+
+ resultDataBundle.putParcelable(Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+ finalResponseReceiver)
+
resultReceiver?.send(
BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION,
resultDataBundle
@@ -286,10 +296,14 @@
fun sendCancellationCode(
cancelCode: Int,
requestToken: IBinder?,
- resultReceiver: ResultReceiver?
+ resultReceiver: ResultReceiver?,
+ finalResponseReceiver: ResultReceiver?
) {
if (requestToken != null && resultReceiver != null) {
val resultData = Bundle()
+ resultData.putParcelable(Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+ finalResponseReceiver)
+
BaseDialogResult.addToBundle(BaseDialogResult(requestToken), resultData)
resultReceiver.send(cancelCode, resultData)
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 05aa548..26a97cd 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -216,13 +216,18 @@
android.credentials.selection.Constants.EXTRA_RESULT_RECEIVER,
ResultReceiver::class.java
)
+ val finalResponseResultReceiver = intent.getParcelableExtra(
+ android.credentials.selection.Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+ ResultReceiver::class.java
+ )
+
val requestInfo = intent.extras?.getParcelable(
RequestInfo.EXTRA_REQUEST_INFO,
RequestInfo::class.java
)
CredentialManagerRepo.sendCancellationCode(
BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE,
- requestInfo?.token, resultReceiver
+ requestInfo?.token, resultReceiver, finalResponseResultReceiver
)
this.finish()
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 2628f09..8fde5d7 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -19,13 +19,12 @@
import android.app.PendingIntent
import android.app.assist.AssistStructure
import android.content.Context
-import android.credentials.Credential
+import android.content.Intent
import android.credentials.CredentialManager
-import android.credentials.CredentialOption
-import android.credentials.GetCandidateCredentialsException
-import android.credentials.GetCandidateCredentialsResponse
import android.credentials.GetCredentialRequest
-import android.credentials.GetCredentialResponse
+import android.credentials.GetCandidateCredentialsResponse
+import android.credentials.GetCandidateCredentialsException
+import android.credentials.CredentialOption
import android.credentials.selection.Entry
import android.credentials.selection.GetCredentialProviderData
import android.credentials.selection.ProviderData
@@ -47,7 +46,6 @@
import android.service.credentials.CredentialProviderService
import android.util.Log
import android.view.autofill.AutofillId
-import android.view.autofill.AutofillValue
import android.view.autofill.IAutoFillManagerClient
import android.widget.RemoteViews
import android.widget.inline.InlinePresentationSpec
@@ -131,30 +129,7 @@
val outcome = object : OutcomeReceiver<GetCandidateCredentialsResponse,
GetCandidateCredentialsException> {
override fun onResult(result: GetCandidateCredentialsResponse) {
- Log.i(TAG, "getCandidateCredentials onResponse")
-
- if (result.getCredentialResponse != null) {
- val autofillId: AutofillId? = result.getCredentialResponse
- .credential.data.getParcelable(
- CredentialProviderService.EXTRA_AUTOFILL_ID,
- AutofillId::class.java)
- Log.i(TAG, "getCandidateCredentials final response, autofillId: " +
- autofillId)
-
- if (autofillId != null) {
- autofillCallback.autofill(
- sessionId,
- mutableListOf(autofillId),
- mutableListOf(
- AutofillValue.forText(
- convertResponseToJson(result.getCredentialResponse)
- )
- ),
- false)
- }
- return
- }
-
+ Log.i(TAG, "getCandidateCredentials onResult")
val fillResponse = convertToFillResponse(result, request,
responseClientState)
if (fillResponse != null) {
@@ -181,57 +156,6 @@
)
}
- // TODO(b/318118018): Use from Jetpack
- private fun convertResponseToJson(response: GetCredentialResponse): String? {
- try {
- val jsonObject = JSONObject()
- jsonObject.put("type", "get")
- val jsonCred = JSONObject()
- jsonCred.put("type", response.credential.type)
- jsonCred.put("data", credentialToJSON(
- response.credential))
- jsonObject.put("credential", jsonCred)
- return jsonObject.toString()
- } catch (e: JSONException) {
- Log.i(
- TAG, "Exception while constructing response JSON: " +
- e.message
- )
- }
- return null
- }
-
- // TODO(b/318118018): Replace with calls to Jetpack
- private fun credentialToJSON(credential: Credential): JSONObject? {
- Log.i(TAG, "credentialToJSON")
- try {
- if (credential.type == "android.credentials.TYPE_PASSWORD_CREDENTIAL") {
- Log.i(TAG, "toJSON PasswordCredential")
-
- val json = JSONObject()
- val id = credential.data.getString("androidx.credentials.BUNDLE_KEY_ID")
- val pass = credential.data.getString("androidx.credentials.BUNDLE_KEY_PASSWORD")
- json.put("androidx.credentials.BUNDLE_KEY_ID", id)
- json.put("androidx.credentials.BUNDLE_KEY_PASSWORD", pass)
- return json
- } else if (credential.type == "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL") {
- Log.i(TAG, "toJSON PublicKeyCredential")
-
- val json = JSONObject()
- val responseJson = credential
- .data
- .getString("androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON")
- json.put("androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON",
- responseJson)
- return json
- }
- } catch (e: JSONException) {
- Log.i(TAG, "issue while converting credential response to JSON")
- }
- Log.i(TAG, "Unsupported credential type")
- return null
- }
-
private fun getEntryToIconMap(
candidateProviderDataList: List<GetCredentialProviderData>
): Map<String, Icon> {
@@ -275,6 +199,7 @@
val autofillIdToProvidersMap: Map<AutofillId, ArrayList<GetCredentialProviderData>> =
mapAutofillIdToProviders(candidateProviders)
val fillResponseBuilder = FillResponse.Builder()
+ fillResponseBuilder.setFlags(FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE)
var validFillResponse = false
autofillIdToProvidersMap.forEach { (autofillId, providers) ->
validFillResponse = processProvidersForAutofillId(
@@ -387,7 +312,7 @@
presentationBuilder.build())
.build())
.setAuthentication(pendingIntent.intentSender)
- .setAuthenticationExtras(fillInIntent.extras)
+ .setCredentialFillInIntent(fillInIntent)
.build())
datasetAdded = true
i++
@@ -407,11 +332,11 @@
}
private fun createInlinePresentation(
- primaryEntry: CredentialEntryInfo,
- pendingIntent: PendingIntent,
- icon: Icon,
- spec: InlinePresentationSpec,
- duplicateDisplayNameForPasskeys: MutableMap<String, Boolean>
+ primaryEntry: CredentialEntryInfo,
+ pendingIntent: PendingIntent,
+ icon: Icon,
+ spec: InlinePresentationSpec,
+ duplicateDisplayNameForPasskeys: MutableMap<String, Boolean>
): InlinePresentation {
val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY &&
primaryEntry.displayName != null) {
@@ -437,7 +362,8 @@
fillResponseBuilder: FillResponse.Builder
) {
val presentationBuilder = Presentations.Builder()
- .setMenuPresentation(RemoteViewsFactory.createMoreSignInOptionsPresentation(this))
+ .setMenuPresentation(
+ RemoteViewsFactory.createMoreSignInOptionsPresentation(this))
fillResponseBuilder.addDataset(
Dataset.Builder()
@@ -477,9 +403,8 @@
.setInlinePresentation(InlinePresentation(
sliceBuilder.build().slice, spec, /* pinned= */ true))
- val extraBundle = Bundle()
- extraBundle.putParcelableArrayList(
- ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, providerDataList)
+ val extrasIntent = Intent()
+ extrasIntent.putExtra(ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, providerDataList)
fillResponseBuilder.addDataset(
dataSetBuilder
@@ -489,7 +414,7 @@
presentationBuilder.build())
.build())
.setAuthentication(bottomSheetPendingIntent.intentSender)
- .setAuthenticationExtras(extraBundle)
+ .setCredentialFillInIntent(extrasIntent)
.build()
)
}
@@ -640,7 +565,6 @@
autofillId: AutofillId,
responseClientState: Bundle
): List<CredentialOption> {
- // TODO(b/293945193) Replace with isCredential check from viewNode
val credentialHints: MutableList<String> = mutableListOf()
if (viewNode.autofillHints != null) {
for (hint in viewNode.autofillHints!!) {
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
index 3297991..5590219 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt
@@ -76,7 +76,7 @@
Chip(
label = labelParam,
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.fillMaxWidth(),
secondaryLabel = secondaryLabelParam,
icon = iconParam,
colors = colors,
@@ -104,7 +104,6 @@
label = stringResource(R.string.dialog_sign_in_options_button),
onClick = onClick,
modifier = Modifier
- .fillMaxWidth()
.padding(top = TOPPADDING)
)
}
@@ -121,7 +120,6 @@
label = stringResource(R.string.dialog_continue_button),
onClick = onClick,
modifier = Modifier
- .fillMaxWidth()
.padding(top = TOPPADDING),
colors = ChipDefaults.primaryChipColors(),
)
@@ -139,7 +137,6 @@
label = stringResource(R.string.dialog_dismiss_button),
onClick = onClick,
modifier = Modifier
- .fillMaxWidth()
.padding(top = TOPPADDING),
)
}
diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml
index f425f52..3242935 100644
--- a/packages/PackageInstaller/res/values/strings.xml
+++ b/packages/PackageInstaller/res/values/strings.xml
@@ -288,7 +288,7 @@
cannot happen immediately because the device is offline (has no internet connection.
[CHAR LIMIT=none] -->
<string name="unarchive_error_offline_body">
- This app will automatically restore when you\'re connected to the internet
+ To restore this app, check your internet connection and try again
</string>
<!-- Dialog title shown when the user is trying to restore an app but a generic error happened.
diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS
index b2b7b61..5f5f1d5 100644
--- a/packages/SettingsLib/OWNERS
+++ b/packages/SettingsLib/OWNERS
@@ -6,7 +6,6 @@
edgarwang@google.com
evanlaird@google.com
juliacr@google.com
-yantingyang@google.com
ykhung@google.com
# Exempt resource files (because they are in a flat directory and too hard to manage via OWNERS)
diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml
index 4b5a9bc..b55dd1b 100644
--- a/packages/SettingsLib/Spa/spa/res/values/themes.xml
+++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml
@@ -24,4 +24,8 @@
</style>
<style name="Theme.SpaLib.Dialog" parent="Theme.Material3.DayNight.Dialog"/>
+ <style name="Theme.SpaLib.BottomSheetDialog" parent="Theme.SpaLib">
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowIsTranslucent">true</item>
+ </style>
</resources>
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
index b34c310..e704505 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
@@ -18,7 +18,6 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
@@ -95,56 +94,60 @@
if (options.isNotEmpty()) {
ExposedDropdownMenu(
expanded = expanded,
- modifier = Modifier
- .fillMaxWidth()
- .width(with(LocalDensity.current) { dropDownWidth.toDp() }),
+ modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }),
onDismissRequest = { expanded = false },
) {
options.forEachIndexed { index, option ->
- TextButton(
- modifier = Modifier
- .fillMaxHeight()
- .fillMaxWidth(),
- onClick = {
- if (selectedOptionsState.contains(index)) {
- if (index == allIndex)
- selectedOptionsState.clear()
- else {
- selectedOptionsState.remove(
- index
- )
- if (selectedOptionsState.contains(allIndex))
- selectedOptionsState.remove(
- allIndex
- )
- }
- } else {
- selectedOptionsState.add(
- index
- )
- }
- onSelectedOptionStateChange()
- }) {
- Row(
- modifier = Modifier
- .fillMaxHeight()
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.Start,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Checkbox(
- checked = selectedOptionsState.contains(index),
- onCheckedChange = null,
- )
- Text(text = option)
- }
- }
+ CheckboxItem(
+ selectedOptionsState,
+ index,
+ allIndex,
+ onSelectedOptionStateChange,
+ option,
+ )
}
}
}
}
}
+@Composable
+private fun CheckboxItem(
+ selectedOptionsState: SnapshotStateList<Int>,
+ index: Int,
+ allIndex: Int,
+ onSelectedOptionStateChange: () -> Unit,
+ option: String
+) {
+ TextButton(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ if (selectedOptionsState.contains(index)) {
+ if (index == allIndex) {
+ selectedOptionsState.clear()
+ } else {
+ selectedOptionsState.remove(index)
+ selectedOptionsState.remove(allIndex)
+ }
+ } else {
+ selectedOptionsState.add(index)
+ }
+ onSelectedOptionStateChange()
+ }) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(
+ checked = selectedOptionsState.contains(index),
+ onCheckedChange = null,
+ )
+ Text(text = option)
+ }
+ }
+}
+
@Preview
@Composable
private fun ActionButtonsPreview() {
@@ -158,4 +161,4 @@
enabled = true,
onSelectedOptionStateChange = {})
}
-}
\ No newline at end of file
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index 0a98791..988afd7 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -17,10 +17,11 @@
package com.android.settingslib.spaprivileged.model.app
import android.content.Context
-import android.content.pm.FeatureFlags
-import android.content.pm.FeatureFlagsImpl
import android.content.Intent
import android.content.pm.ApplicationInfo
+import android.content.pm.FeatureFlags
+import android.content.pm.FeatureFlagsImpl
+import android.content.pm.Flags
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ApplicationInfoFlags
import android.content.pm.ResolveInfo
@@ -85,13 +86,7 @@
loadInstantApps: Boolean,
matchAnyUserForAdmin: Boolean,
): List<ApplicationInfo> = coroutineScope {
- val hiddenSystemModulesDeferred = async {
- packageManager.getInstalledModules(0)
- .filter { it.isHidden }
- .map { it.packageName }
- .filterNotNull()
- .toSet()
- }
+ val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() }
val hideWhenDisabledPackagesDeferred = async {
context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)
}
@@ -205,6 +200,15 @@
private fun isSystemApp(app: ApplicationInfo, homeOrLauncherPackages: Set<String>): Boolean =
app.isSystemApp && !app.isUpdatedSystemApp && app.packageName !in homeOrLauncherPackages
+ private fun PackageManager.getHiddenSystemModules(): Set<String> {
+ val moduleInfos = getInstalledModules(0).filter { it.isHidden }
+ val hiddenApps = moduleInfos.mapNotNull { it.packageName }.toMutableSet()
+ if (Flags.provideInfoOfApkInApex()) {
+ hiddenApps += moduleInfos.flatMap { it.apkInApexPackageNames }
+ }
+ return hiddenApps
+ }
+
companion object {
private fun ApplicationInfo.isInAppList(
showInstantApps: Boolean,
diff --git a/packages/SettingsLib/SpaPrivileged/tests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/Android.bp
index a28ebc6..458fcc9 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/tests/Android.bp
@@ -35,5 +35,6 @@
"androidx.test.ext.junit",
"androidx.test.runner",
"mockito-target-minus-junit4",
+ "flag-junit",
],
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index f292231..efd53a4 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -21,6 +21,7 @@
import android.content.pm.ApplicationInfo
import android.content.pm.FakeFeatureFlagsImpl
import android.content.pm.Flags
+import android.content.pm.ModuleInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ApplicationInfoFlags
import android.content.pm.PackageManager.ResolveInfoFlags
@@ -28,6 +29,7 @@
import android.content.pm.UserInfo
import android.content.res.Resources
import android.os.UserManager
+import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.internal.R
@@ -35,6 +37,7 @@
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -50,6 +53,9 @@
@RunWith(AndroidJUnit4::class)
class AppListRepositoryTest {
+ @get:Rule
+ val mSetFlagsRule = SetFlagsRule()
+
private val resources = mock<Resources> {
on { getStringArray(R.array.config_hideWhenDisabled_packageNames) } doReturn emptyArray()
}
@@ -273,6 +279,38 @@
}
@Test
+ fun loadApps_hasApkInApexInfo_shouldNotIncludeAllHiddenApps() = runTest {
+ mSetFlagsRule.enableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
+ packageManager.stub {
+ on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE)
+ }
+ mockInstalledApplications(
+ listOf(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP),
+ ADMIN_USER_ID
+ )
+
+ val appList = repository.loadApps(userId = ADMIN_USER_ID)
+
+ assertThat(appList).containsExactly(NORMAL_APP)
+ }
+
+ @Test
+ fun loadApps_noApkInApexInfo_shouldNotIncludeHiddenSystemModule() = runTest {
+ mSetFlagsRule.disableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
+ packageManager.stub {
+ on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE)
+ }
+ mockInstalledApplications(
+ listOf(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP),
+ ADMIN_USER_ID
+ )
+
+ val appList = repository.loadApps(userId = ADMIN_USER_ID)
+
+ assertThat(appList).containsExactly(NORMAL_APP, HIDDEN_APEX_APP)
+ }
+
+ @Test
fun showSystemPredicate_showSystem() = runTest {
val app = SYSTEM_APP
@@ -402,6 +440,20 @@
isArchived = true
}
+ val HIDDEN_APEX_APP = ApplicationInfo().apply {
+ packageName = "hidden.apex.package"
+ }
+
+ val HIDDEN_MODULE_APP = ApplicationInfo().apply {
+ packageName = "hidden.module.package"
+ }
+
+ val HIDDEN_MODULE = ModuleInfo().apply {
+ packageName = "hidden.module.package"
+ apkInApexPackageNames = listOf("hidden.apex.package")
+ isHidden = true
+ }
+
fun resolveInfoOf(packageName: String) = ResolveInfo().apply {
activityInfo = ActivityInfo().apply {
this.packageName = packageName
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
index 07de7fd..c482995 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
@@ -22,6 +22,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
+import android.content.pm.Flags;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@@ -164,12 +165,16 @@
try {
final PackageInfo pkg = pm.getPackageInfo(packageName, 0 /* flags */);
- // Check if the package is contained in an APEX. There is no public API to properly
- // check whether a given APK package comes from an APEX registered as module.
- // Therefore we conservatively assume that any package scanned from an /apex path is
- // a system package.
- return pkg.applicationInfo.sourceDir.startsWith(
- Environment.getApexDirectory().getAbsolutePath());
+ if (Flags.provideInfoOfApkInApex()) {
+ return pkg.getApexPackageName() != null;
+ } else {
+ // Check if the package is contained in an APEX. There is no public API to properly
+ // check whether a given APK package comes from an APEX registered as module.
+ // Therefore we conservatively assume that any package scanned from an /apex path is
+ // a system package.
+ return pkg.applicationInfo.sourceDir.startsWith(
+ Environment.getApexDirectory().getAbsolutePath());
+ }
} catch (PackageManager.NameNotFoundException e) {
return false;
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 8e1067f..e3012cd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -27,6 +27,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
+import android.content.pm.Flags;
import android.content.pm.IPackageManager;
import android.content.pm.IPackageStatsObserver;
import android.content.pm.ModuleInfo;
@@ -226,6 +227,11 @@
final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */);
for (ModuleInfo info : moduleInfos) {
mSystemModules.put(info.getPackageName(), info.isHidden());
+ if (Flags.provideInfoOfApkInApex()) {
+ for (String apkInApexPackageName : info.getApkInApexPackageNames()) {
+ mSystemModules.put(apkInApexPackageName, info.isHidden());
+ }
+ }
}
/**
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/model/RoutingSession.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/model/RoutingSession.kt
new file mode 100644
index 0000000..a98f3e2
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/model/RoutingSession.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.volume.data.model
+
+import android.media.RoutingSessionInfo
+
+/** Models a routing session which is created when a media route is selected. */
+data class RoutingSession(
+ val routingSessionInfo: RoutingSessionInfo,
+ val isVolumeSeekBarEnabled: Boolean,
+ val isMediaOutputDisabled: Boolean,
+)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index 6761aa7..f729c04 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -16,15 +16,12 @@
package com.android.settingslib.volume.data.repository
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager.OnCommunicationDeviceChangedListener
import androidx.concurrent.futures.DirectExecutor
import com.android.internal.util.ConcurrentUtils
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.settingslib.volume.shared.model.AudioStreamModel
import com.android.settingslib.volume.shared.model.RingerMode
@@ -32,7 +29,6 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
@@ -40,7 +36,6 @@
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -77,7 +72,7 @@
}
class AudioRepositoryImpl(
- private val context: Context,
+ private val audioManagerIntentsReceiver: AudioManagerIntentsReceiver,
private val audioManager: AudioManager,
private val backgroundCoroutineContext: CoroutineContext,
private val coroutineScope: CoroutineScope,
@@ -93,30 +88,9 @@
.flowOn(backgroundCoroutineContext)
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode)
- private val audioManagerIntents: SharedFlow<String> =
- callbackFlow {
- val receiver =
- object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent) {
- intent.action?.let { action -> launch { send(action) } }
- }
- }
- context.registerReceiver(
- receiver,
- IntentFilter().apply {
- for (action in allActions) {
- addAction(action)
- }
- }
- )
-
- awaitClose { context.unregisterReceiver(receiver) }
- }
- .shareIn(coroutineScope, SharingStarted.WhileSubscribed())
-
override val ringerMode: StateFlow<RingerMode> =
- audioManagerIntents
- .filter { ringerActions.contains(it) }
+ audioManagerIntentsReceiver.intents
+ .filter { AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION == it.action }
.map { RingerMode(audioManager.ringerModeInternal) }
.flowOn(backgroundCoroutineContext)
.stateIn(
@@ -146,8 +120,7 @@
)
override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> {
- return audioManagerIntents
- .filter { modelActions.contains(it) }
+ return audioManagerIntentsReceiver.intents
.map { getCurrentAudioStream(audioStream) }
.flowOn(backgroundCoroutineContext)
}
@@ -189,20 +162,4 @@
// return STREAM_VOICE_CALL in getAudioStream
audioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL)
}
-
- private companion object {
- val modelActions =
- setOf(
- AudioManager.STREAM_MUTE_CHANGED_ACTION,
- AudioManager.MASTER_MUTE_CHANGED_ACTION,
- AudioManager.VOLUME_CHANGED_ACTION,
- AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
- AudioManager.STREAM_DEVICES_CHANGED_ACTION,
- )
- val ringerActions =
- setOf(
- AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
- )
- val allActions = ringerActions + modelActions
- }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
index 1597b77..aa9ae76 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
@@ -15,8 +15,13 @@
*/
package com.android.settingslib.volume.data.repository
+import android.media.AudioManager
+import android.media.MediaRouter2Manager
+import android.media.RoutingSessionInfo
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.volume.data.model.RoutingSession
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
@@ -24,10 +29,15 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
/** Repository providing data about connected media devices. */
interface LocalMediaRepository {
@@ -37,43 +47,55 @@
/** Currently connected media device */
val currentConnectedDevice: StateFlow<MediaDevice?>
+
+ val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>
+
+ suspend fun adjustSessionVolume(sessionId: String?, volume: Int)
}
class LocalMediaRepositoryImpl(
+ audioManagerIntentsReceiver: AudioManagerIntentsReceiver,
private val localMediaManager: LocalMediaManager,
+ private val mediaRouter2Manager: MediaRouter2Manager,
coroutineScope: CoroutineScope,
- backgroundContext: CoroutineContext,
+ private val backgroundContext: CoroutineContext,
) : LocalMediaRepository {
- private val deviceUpdates: Flow<DevicesUpdate> = callbackFlow {
- val callback =
- object : LocalMediaManager.DeviceCallback {
- override fun onDeviceListUpdate(newDevices: List<MediaDevice>?) {
- trySend(DevicesUpdate.DeviceListUpdate(newDevices ?: emptyList()))
- }
+ private val devicesChanges =
+ audioManagerIntentsReceiver.intents.filter {
+ AudioManager.STREAM_DEVICES_CHANGED_ACTION == it.action
+ }
+ private val mediaDevicesUpdates: Flow<DevicesUpdate> =
+ callbackFlow {
+ val callback =
+ object : LocalMediaManager.DeviceCallback {
+ override fun onDeviceListUpdate(newDevices: List<MediaDevice>?) {
+ trySend(DevicesUpdate.DeviceListUpdate(newDevices ?: emptyList()))
+ }
- override fun onSelectedDeviceStateChanged(
- device: MediaDevice?,
- state: Int,
- ) {
- trySend(DevicesUpdate.SelectedDeviceStateChanged)
- }
+ override fun onSelectedDeviceStateChanged(
+ device: MediaDevice?,
+ state: Int,
+ ) {
+ trySend(DevicesUpdate.SelectedDeviceStateChanged)
+ }
- override fun onDeviceAttributesChanged() {
- trySend(DevicesUpdate.DeviceAttributesChanged)
+ override fun onDeviceAttributesChanged() {
+ trySend(DevicesUpdate.DeviceAttributesChanged)
+ }
+ }
+ localMediaManager.registerCallback(callback)
+ localMediaManager.startScan()
+
+ awaitClose {
+ localMediaManager.stopScan()
+ localMediaManager.unregisterCallback(callback)
}
}
- localMediaManager.registerCallback(callback)
- localMediaManager.startScan()
-
- awaitClose {
- localMediaManager.stopScan()
- localMediaManager.unregisterCallback(callback)
- }
- }
+ .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
override val mediaDevices: StateFlow<Collection<MediaDevice>> =
- deviceUpdates
+ mediaDevicesUpdates
.mapNotNull {
if (it is DevicesUpdate.DeviceListUpdate) {
it.newDevices ?: emptyList()
@@ -85,7 +107,7 @@
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
override val currentConnectedDevice: StateFlow<MediaDevice?> =
- deviceUpdates
+ merge(devicesChanges, mediaDevicesUpdates)
.map { localMediaManager.currentConnectedDevice }
.stateIn(
coroutineScope,
@@ -93,6 +115,30 @@
localMediaManager.currentConnectedDevice
)
+ override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> =
+ merge(devicesChanges, mediaDevicesUpdates)
+ .onStart { emit(Unit) }
+ .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) }
+ .flowOn(backgroundContext)
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
+
+ override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
+ withContext(backgroundContext) {
+ if (sessionId == null) {
+ localMediaManager.adjustSessionVolume(volume)
+ } else {
+ localMediaManager.adjustSessionVolume(sessionId, volume)
+ }
+ }
+ }
+
+ private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession =
+ RoutingSession(
+ info,
+ isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(),
+ isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info)
+ )
+
private sealed interface DevicesUpdate {
data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
index 93aa90d..ab8c6b8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
@@ -16,30 +16,23 @@
package com.android.settingslib.volume.data.repository
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
import android.media.AudioManager
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.media.session.PlaybackState
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.bluetooth.headsetAudioModeChanges
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
/** Provides controllers for currently active device media sessions. */
interface MediaControllerRepository {
@@ -49,40 +42,25 @@
}
class MediaControllerRepositoryImpl(
- private val context: Context,
+ audioManagerIntentsReceiver: AudioManagerIntentsReceiver,
private val mediaSessionManager: MediaSessionManager,
localBluetoothManager: LocalBluetoothManager?,
coroutineScope: CoroutineScope,
backgroundContext: CoroutineContext,
) : MediaControllerRepository {
- private val devicesChanges: Flow<Unit> =
- callbackFlow {
- val receiver =
- object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- if (AudioManager.STREAM_DEVICES_CHANGED_ACTION == intent?.action) {
- launch { send(Unit) }
- }
- }
- }
- context.registerReceiver(
- receiver,
- IntentFilter(AudioManager.STREAM_DEVICES_CHANGED_ACTION)
- )
-
- awaitClose { context.unregisterReceiver(receiver) }
- }
- .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
-
+ private val devicesChanges =
+ audioManagerIntentsReceiver.intents.filter {
+ AudioManager.STREAM_DEVICES_CHANGED_ACTION == it.action
+ }
override val activeMediaController: StateFlow<MediaController?> =
- combine(
- localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) }
- ?: emptyFlow(),
- devicesChanges.onStart { emit(Unit) },
- ) { _, _ ->
- getActiveLocalMediaController()
+ buildList {
+ localBluetoothManager?.headsetAudioModeChanges?.let { add(it) }
+ add(devicesChanges)
}
+ .merge()
+ .onStart { emit(Unit) }
+ .map { getActiveLocalMediaController() }
.flowOn(backgroundContext)
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
new file mode 100644
index 0000000..f621335
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.volume.domain.interactor
+
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.volume.data.repository.LocalMediaRepository
+import com.android.settingslib.volume.domain.model.RoutingSession
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+class LocalMediaInteractor(
+ private val repository: LocalMediaRepository,
+ coroutineScope: CoroutineScope,
+) {
+
+ /** Available devices list */
+ val mediaDevices: StateFlow<Collection<MediaDevice>>
+ get() = repository.mediaDevices
+
+ /** Currently connected media device */
+ val currentConnectedDevice: StateFlow<MediaDevice?>
+ get() = repository.currentConnectedDevice
+
+ val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
+ repository.remoteRoutingSessions
+ .map { sessions ->
+ sessions.map {
+ RoutingSession(
+ routingSessionInfo = it.routingSessionInfo,
+ isMediaOutputDisabled = it.isMediaOutputDisabled,
+ isVolumeSeekBarEnabled =
+ it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0
+ )
+ }
+ }
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
+
+ suspend fun adjustSessionVolume(sessionId: String?, volume: Int) =
+ repository.adjustSessionVolume(sessionId, volume)
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/model/RoutingSession.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/model/RoutingSession.kt
new file mode 100644
index 0000000..dfc4703
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/model/RoutingSession.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.volume.domain.model
+
+import android.media.RoutingSessionInfo
+
+/** Models a routing session which is created when a media route is selected. */
+data class RoutingSession(
+ val routingSessionInfo: RoutingSessionInfo,
+ val isMediaOutputDisabled: Boolean,
+ val isVolumeSeekBarEnabled: Boolean,
+)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerIntentsReceiver.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerIntentsReceiver.kt
new file mode 100644
index 0000000..9fa4c86
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioManagerIntentsReceiver.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.volume.shared
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.media.AudioManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
+
+/** Exposes [AudioManager] intents as a observable shared flow. */
+interface AudioManagerIntentsReceiver {
+
+ val intents: SharedFlow<Intent>
+}
+
+class AudioManagerIntentsReceiverImpl(
+ private val context: Context,
+ coroutineScope: CoroutineScope,
+) : AudioManagerIntentsReceiver {
+
+ private val allActions: Collection<String>
+ get() =
+ setOf(
+ AudioManager.STREAM_MUTE_CHANGED_ACTION,
+ AudioManager.MASTER_MUTE_CHANGED_ACTION,
+ AudioManager.VOLUME_CHANGED_ACTION,
+ AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
+ AudioManager.STREAM_DEVICES_CHANGED_ACTION,
+ )
+
+ override val intents: SharedFlow<Intent> =
+ callbackFlow {
+ val receiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ launch { send(intent) }
+ }
+ }
+ context.registerReceiver(
+ receiver,
+ IntentFilter().apply {
+ for (action in allActions) {
+ addAction(action)
+ }
+ }
+ )
+
+ awaitClose { context.unregisterReceiver(receiver) }
+ }
+ .filterNotNull()
+ .filter { intent -> allActions.contains(intent.action) }
+ .shareIn(coroutineScope, SharingStarted.WhileSubscribed())
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
index 7b70c64..48b04db 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
@@ -16,13 +16,11 @@
package com.android.settingslib.volume.data.repository
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
import android.media.AudioDeviceInfo
import android.media.AudioManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.settingslib.volume.shared.FakeAudioManagerIntentsReceiver
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.settingslib.volume.shared.model.AudioStreamModel
import com.android.settingslib.volume.shared.model.RingerMode
@@ -30,6 +28,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -51,17 +50,16 @@
@RunWith(AndroidJUnit4::class)
class AudioRepositoryTest {
- @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
@Captor
private lateinit var modeListenerCaptor: ArgumentCaptor<AudioManager.OnModeChangedListener>
@Captor
private lateinit var communicationDeviceListenerCaptor:
ArgumentCaptor<AudioManager.OnCommunicationDeviceChangedListener>
- @Mock private lateinit var context: Context
@Mock private lateinit var audioManager: AudioManager
@Mock private lateinit var communicationDevice: AudioDeviceInfo
+ private val intentsReceiver = FakeAudioManagerIntentsReceiver()
private val volumeByStream: MutableMap<Int, Int> = mutableMapOf()
private val isAffectedByRingerModeByStream: MutableMap<Int, Boolean> = mutableMapOf()
private val isMuteByStream: MutableMap<Int, Boolean> = mutableMapOf()
@@ -98,7 +96,7 @@
underTest =
AudioRepositoryImpl(
- context,
+ intentsReceiver,
audioManager,
testScope.testScheduler,
testScope.backgroundScope,
@@ -270,8 +268,7 @@
}
private fun triggerIntent(action: String) {
- verify(context).registerReceiver(receiverCaptor.capture(), any())
- receiverCaptor.value.onReceive(context, Intent(action))
+ testScope.launch { intentsReceiver.triggerIntent(action) }
}
private companion object {
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeLocalMediaRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeLocalMediaRepository.kt
new file mode 100644
index 0000000..642b72c
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeLocalMediaRepository.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.volume.data.repository
+
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.volume.data.model.RoutingSession
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeLocalMediaRepository : LocalMediaRepository {
+
+ private val volumeBySession: MutableMap<String?, Int> = mutableMapOf()
+
+ private val mutableMediaDevices = MutableStateFlow<Collection<MediaDevice>>(emptyList())
+ override val mediaDevices: StateFlow<Collection<MediaDevice>>
+ get() = mutableMediaDevices.asStateFlow()
+
+ private val mutableCurrentConnectedDevice = MutableStateFlow<MediaDevice?>(null)
+ override val currentConnectedDevice: StateFlow<MediaDevice?>
+ get() = mutableCurrentConnectedDevice.asStateFlow()
+
+ private val mutableRemoteRoutingSessions =
+ MutableStateFlow<Collection<RoutingSession>>(emptyList())
+ override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>
+ get() = mutableRemoteRoutingSessions.asStateFlow()
+
+ fun updateMediaDevices(devices: Collection<MediaDevice>) {
+ mutableMediaDevices.value = devices
+ }
+
+ fun updateCurrentConnectedDevice(device: MediaDevice?) {
+ mutableCurrentConnectedDevice.value = device
+ }
+
+ fun updateRemoteRoutingSessions(sessions: List<RoutingSession>) {
+ mutableRemoteRoutingSessions.value = sessions
+ }
+
+ fun getSessionVolume(sessionId: String?): Int = volumeBySession.getOrDefault(sessionId, 0)
+
+ override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
+ volumeBySession[sessionId] = volume
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
index d106bce..dc9ea10 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
@@ -15,10 +15,15 @@
*/
package com.android.settingslib.volume.data.repository
+import android.media.MediaRoute2Info
+import android.media.MediaRouter2Manager
+import android.media.RoutingSessionInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.volume.data.model.RoutingSession
+import com.android.settingslib.volume.shared.FakeAudioManagerIntentsReceiver
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
@@ -32,6 +37,10 @@
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@@ -44,10 +53,12 @@
@Mock private lateinit var localMediaManager: LocalMediaManager
@Mock private lateinit var mediaDevice1: MediaDevice
@Mock private lateinit var mediaDevice2: MediaDevice
+ @Mock private lateinit var mediaRouter2Manager: MediaRouter2Manager
@Captor
private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback>
+ private val intentsReceiver = FakeAudioManagerIntentsReceiver()
private val testScope = TestScope()
private lateinit var underTest: LocalMediaRepository
@@ -58,7 +69,9 @@
underTest =
LocalMediaRepositoryImpl(
+ intentsReceiver,
localMediaManager,
+ mediaRouter2Manager,
testScope.backgroundScope,
testScope.testScheduler,
)
@@ -97,4 +110,78 @@
assertThat(currentConnectedDevice).isEqualTo(mediaDevice1)
}
}
+
+ @Test
+ fun kek() {
+ testScope.runTest {
+ `when`(localMediaManager.remoteRoutingSessions)
+ .thenReturn(
+ listOf(
+ testRoutingSessionInfo1,
+ testRoutingSessionInfo2,
+ testRoutingSessionInfo3,
+ )
+ )
+ `when`(localMediaManager.shouldEnableVolumeSeekBar(any())).then {
+ (it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo1
+ }
+ `when`(mediaRouter2Manager.getTransferableRoutes(any<RoutingSessionInfo>())).then {
+ if ((it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo2) {
+ return@then listOf(mock(MediaRoute2Info::class.java))
+ }
+ emptyList<MediaRoute2Info>()
+ }
+ var remoteRoutingSessions: Collection<RoutingSession>? = null
+ underTest.remoteRoutingSessions
+ .onEach { remoteRoutingSessions = it }
+ .launchIn(backgroundScope)
+
+ runCurrent()
+
+ assertThat(remoteRoutingSessions)
+ .containsExactlyElementsIn(
+ listOf(
+ RoutingSession(
+ routingSessionInfo = testRoutingSessionInfo1,
+ isVolumeSeekBarEnabled = true,
+ isMediaOutputDisabled = true,
+ ),
+ RoutingSession(
+ routingSessionInfo = testRoutingSessionInfo2,
+ isVolumeSeekBarEnabled = false,
+ isMediaOutputDisabled = false,
+ ),
+ RoutingSession(
+ routingSessionInfo = testRoutingSessionInfo3,
+ isVolumeSeekBarEnabled = false,
+ isMediaOutputDisabled = true,
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun adjustSessionVolume_adjusts() {
+ testScope.runTest {
+ var volume = 0
+ `when`(localMediaManager.adjustSessionVolume(anyString(), anyInt())).then {
+ volume = it.arguments[1] as Int
+ Unit
+ }
+
+ underTest.adjustSessionVolume("test_session", 10)
+
+ assertThat(volume).isEqualTo(10)
+ }
+ }
+
+ private companion object {
+ val testRoutingSessionInfo1 =
+ RoutingSessionInfo.Builder("id_1", "test.pkg.1").addSelectedRoute("route_1").build()
+ val testRoutingSessionInfo2 =
+ RoutingSessionInfo.Builder("id_2", "test.pkg.2").addSelectedRoute("route_2").build()
+ val testRoutingSessionInfo3 =
+ RoutingSessionInfo.Builder("id_3", "test.pkg.3").addSelectedRoute("route_3").build()
+ }
}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
index f07b1bff..430d733 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
@@ -16,9 +16,6 @@
package com.android.settingslib.volume.data.repository
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
import android.media.AudioManager
import android.media.session.MediaController
import android.media.session.MediaController.PlaybackInfo
@@ -29,6 +26,7 @@
import com.android.settingslib.bluetooth.BluetoothCallback
import com.android.settingslib.bluetooth.BluetoothEventManager
import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.volume.shared.FakeAudioManagerIntentsReceiver
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
@@ -52,10 +50,8 @@
@SmallTest
class MediaControllerRepositoryImplTest {
- @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
@Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback>
- @Mock private lateinit var context: Context
@Mock private lateinit var mediaSessionManager: MediaSessionManager
@Mock private lateinit var localBluetoothManager: LocalBluetoothManager
@Mock private lateinit var eventManager: BluetoothEventManager
@@ -70,6 +66,7 @@
@Mock private lateinit var localPlaybackInfo: PlaybackInfo
private val testScope = TestScope()
+ private val intentsReceiver = FakeAudioManagerIntentsReceiver()
private lateinit var underTest: MediaControllerRepository
@@ -97,7 +94,7 @@
underTest =
MediaControllerRepositoryImpl(
- context,
+ intentsReceiver,
mediaSessionManager,
localBluetoothManager,
testScope.backgroundScope,
@@ -124,7 +121,7 @@
.launchIn(backgroundScope)
runCurrent()
- triggerDevicesChange()
+ intentsReceiver.triggerIntent(AudioManager.STREAM_DEVICES_CHANGED_ACTION)
triggerOnAudioModeChanged()
runCurrent()
@@ -149,7 +146,7 @@
.launchIn(backgroundScope)
runCurrent()
- triggerDevicesChange()
+ intentsReceiver.triggerIntent(AudioManager.STREAM_DEVICES_CHANGED_ACTION)
triggerOnAudioModeChanged()
runCurrent()
@@ -157,22 +154,19 @@
}
}
- private fun triggerDevicesChange() {
- verify(context).registerReceiver(receiverCaptor.capture(), any())
- receiverCaptor.value.onReceive(context, Intent(AudioManager.STREAM_DEVICES_CHANGED_ACTION))
- }
-
private fun triggerOnAudioModeChanged() {
verify(eventManager).registerCallback(callbackCaptor.capture())
callbackCaptor.value.onAudioModeChanged()
}
private companion object {
- val statePlaying =
+ val statePlaying: PlaybackState =
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build()
- val stateError = PlaybackState.Builder().setState(PlaybackState.STATE_ERROR, 0, 0f).build()
- val stateStopped =
+ val stateError: PlaybackState =
+ PlaybackState.Builder().setState(PlaybackState.STATE_ERROR, 0, 0f).build()
+ val stateStopped: PlaybackState =
PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0, 0f).build()
- val stateNone = PlaybackState.Builder().setState(PlaybackState.STATE_NONE, 0, 0f).build()
+ val stateNone: PlaybackState =
+ PlaybackState.Builder().setState(PlaybackState.STATE_NONE, 0, 0f).build()
}
}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/FakeAudioManagerIntentsReceiver.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/FakeAudioManagerIntentsReceiver.kt
new file mode 100644
index 0000000..530690a
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/shared/FakeAudioManagerIntentsReceiver.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.volume.shared
+
+import android.content.Intent
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+
+class FakeAudioManagerIntentsReceiver : AudioManagerIntentsReceiver {
+
+ private val mutableIntents = MutableSharedFlow<Intent>()
+ override val intents: SharedFlow<Intent> = mutableIntents.asSharedFlow()
+
+ suspend fun triggerIntent(intent: Intent) {
+ mutableIntents.emit(intent)
+ }
+
+ suspend fun triggerIntent(action: String) {
+ triggerIntent(Intent(action))
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java
index 994c1ee..e32d880 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java
@@ -16,6 +16,8 @@
package com.android.settingslib.applications;
+import static android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
@@ -24,12 +26,16 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
+import android.os.Environment;
+import android.platform.test.flag.junit.SetFlagsRule;
import com.android.settingslib.Utils;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -39,6 +45,8 @@
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowPackageManager;
import java.io.File;
import java.lang.reflect.Field;
@@ -60,6 +68,10 @@
private ApplicationInfo mAppInfo;
private ApplicationsState.AppEntry mAppEntry;
private ArrayList<ApplicationsState.AppEntry> mAppEntries;
+ private ShadowPackageManager mShadowPackageManager;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setUp() {
@@ -70,6 +82,7 @@
mAppEntry = createAppEntry(mAppInfo, /* id= */ 1);
mAppEntries = new ArrayList<>(Arrays.asList(mAppEntry));
doReturn(mIcon).when(mIcon).mutate();
+ mShadowPackageManager = Shadow.extract(mContext.getPackageManager());
}
@After
@@ -151,6 +164,32 @@
assertThat(AppUtils.isAppInstalled(appEntry)).isFalse();
}
+ @Test
+ public void isMainlineModule_hasApexPackageName_shouldCheckByPackageInfo() {
+ mSetFlagsRule.enableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = APP_PACKAGE_NAME;
+ packageInfo.setApexPackageName("com.test.apex.package");
+ mShadowPackageManager.installPackage(packageInfo);
+
+ assertThat(
+ AppUtils.isMainlineModule(mContext.getPackageManager(), APP_PACKAGE_NAME)).isTrue();
+ }
+
+ @Test
+ public void isMainlineModule_noApexPackageName_shouldCheckBySourceDirPath() {
+ mSetFlagsRule.disableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
+ ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.sourceDir = Environment.getApexDirectory().getAbsolutePath();
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = APP_PACKAGE_NAME;
+ packageInfo.applicationInfo = applicationInfo;
+ mShadowPackageManager.installPackage(packageInfo);
+
+ assertThat(
+ AppUtils.isMainlineModule(mContext.getPackageManager(), APP_PACKAGE_NAME)).isTrue();
+ }
+
private ApplicationsState.AppEntry createAppEntry(ApplicationInfo appInfo, int id) {
ApplicationsState.AppEntry appEntry = new ApplicationsState.AppEntry(mContext, appInfo, id);
appEntry.label = "label";
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
index 34d8148..827d8fa 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
@@ -16,6 +16,7 @@
package com.android.settingslib.applications;
+import static android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX;
import static android.os.UserHandle.MU_ENABLED;
import static android.os.UserHandle.USER_SYSTEM;
@@ -58,6 +59,7 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.text.TextUtils;
import android.util.IconDrawableFactory;
@@ -70,6 +72,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -89,6 +92,7 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
@@ -137,6 +141,9 @@
@Mock
private IPackageManager mPackageManagerService;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Implements(value = IconDrawableFactory.class)
public static class ShadowIconDrawableFactory {
@@ -169,7 +176,9 @@
public List<ModuleInfo> getInstalledModules(int flags) {
if (mInstalledModules.isEmpty()) {
for (String moduleName : mModuleNames) {
- mInstalledModules.add(createModuleInfo(moduleName));
+ mInstalledModules.add(
+ createModuleInfo(moduleName,
+ TextUtils.concat(moduleName, ".apex").toString()));
}
}
return mInstalledModules;
@@ -188,10 +197,11 @@
return resolveInfos;
}
- private ModuleInfo createModuleInfo(String packageName) {
+ private ModuleInfo createModuleInfo(String packageName, String apexPackageName) {
final ModuleInfo info = new ModuleInfo();
info.setName(packageName);
info.setPackageName(packageName);
+ info.setApkInApexPackageNames(Collections.singletonList(apexPackageName));
// will treat any app with package name that contains "hidden" as hidden module
info.setHidden(!TextUtils.isEmpty(packageName) && packageName.contains("hidden"));
return info;
@@ -822,4 +832,32 @@
assertThat(mApplicationsState.getEntry(PKG_1, /* userId= */ 0).info.packageName)
.isEqualTo(PKG_1);
}
+
+ @Test
+ public void isHiddenModule_hasApkInApexInfo_shouldSupportHiddenApexPackage() {
+ mSetFlagsRule.enableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
+ ApplicationsState.sInstance = null;
+ mApplicationsState = ApplicationsState.getInstance(mApplication, mPackageManagerService);
+ String normalModulePackage = "test.module.1";
+ String hiddenModulePackage = "test.hidden.module.2";
+ String hiddenApexPackage = "test.hidden.module.2.apex";
+
+ assertThat(mApplicationsState.isHiddenModule(normalModulePackage)).isFalse();
+ assertThat(mApplicationsState.isHiddenModule(hiddenModulePackage)).isTrue();
+ assertThat(mApplicationsState.isHiddenModule(hiddenApexPackage)).isTrue();
+ }
+
+ @Test
+ public void isHiddenModule_noApkInApexInfo_onlySupportHiddenModule() {
+ mSetFlagsRule.disableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
+ ApplicationsState.sInstance = null;
+ mApplicationsState = ApplicationsState.getInstance(mApplication, mPackageManagerService);
+ String normalModulePackage = "test.module.1";
+ String hiddenModulePackage = "test.hidden.module.2";
+ String hiddenApexPackage = "test.hidden.module.2.apex";
+
+ assertThat(mApplicationsState.isHiddenModule(normalModulePackage)).isFalse();
+ assertThat(mApplicationsState.isHiddenModule(hiddenModulePackage)).isTrue();
+ assertThat(mApplicationsState.isHiddenModule(hiddenApexPackage)).isFalse();
+ }
}
diff --git a/packages/SoundPicker/OWNERS b/packages/SoundPicker/OWNERS
deleted file mode 100644
index 5bf46e0..0000000
--- a/packages/SoundPicker/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# Haptics team works on the SoundPicker
-include platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/packages/SoundPicker2/Android.bp b/packages/SoundPicker2/Android.bp
deleted file mode 100644
index f4d8bf2..0000000
--- a/packages/SoundPicker2/Android.bp
+++ /dev/null
@@ -1,46 +0,0 @@
-package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "frameworks_base_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_library {
- name: "SoundPicker2Lib",
- srcs: [
- "src/**/*.java",
- ],
- resource_dirs: [
- "res",
- ],
- static_libs: [
- "androidx.appcompat_appcompat",
- "hilt_android",
- "guava",
- "androidx.recyclerview_recyclerview",
- "androidx-constraintlayout_constraintlayout",
- "androidx.viewpager2_viewpager2",
- "com.google.android.material_material",
- ],
-}
-
-android_app {
- name: "SoundPicker2",
- defaults: ["platform_app_defaults"],
- manifest: "AndroidManifest.xml",
- static_libs: ["SoundPicker2Lib"],
- platform_apis: true,
- certificate: "media",
- privileged: true,
-
- optimize: {
- enabled: true,
- optimize: true,
- shrink: true,
- shrink_resources: true,
- obfuscate: false,
- proguard_compatibility: false,
- },
-}
diff --git a/packages/SoundPicker2/AndroidManifest.xml b/packages/SoundPicker2/AndroidManifest.xml
deleted file mode 100644
index 934b003..0000000
--- a/packages/SoundPicker2/AndroidManifest.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.soundpicker"
- android:sharedUserId="android.media">
-
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-
- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
- <uses-permission android:name="android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY" />
- <uses-permission android:name="android.permission.WRITE_SETTINGS" />
-
- <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
-
- <application
- android:name=".RingtonePickerApplication"
- android:allowBackup="false"
- android:label="@string/app_label"
- android:theme="@style/Theme.AppCompat"
- android:supportsRtl="true">
- <receiver android:name="RingtoneReceiver"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.DEVICE_CUSTOMIZATION_READY"/>
- </intent-filter>
- </receiver>
-
- <service android:name="RingtoneOverlayService" />
-
- <activity android:name="RingtonePickerActivity"
- android:theme="@style/Theme.AppCompat.Dialog"
- android:enabled="@*android:bool/config_defaultRingtonePickerEnabled"
- android:excludeFromRecents="true"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.RINGTONE_PICKER" />
- <category android:name="android.intent.category.DEFAULT" />
- <category android:name="android.intent.category.RINGTONE_PICKER_SOUND" />
- <category android:name="android.intent.category.RINGTONE_PICKER_VIBRATION" />
- <category android:name="android.intent.category.RINGTONE_PICKER_RINGTONE" />
- </intent-filter>
- </activity>
- </application>
-</manifest>
diff --git a/packages/SoundPicker2/OWNERS b/packages/SoundPicker2/OWNERS
deleted file mode 100644
index 5bf46e0..0000000
--- a/packages/SoundPicker2/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# Haptics team works on the SoundPicker
-include platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/packages/SoundPicker2/res/drawable/ic_add.xml b/packages/SoundPicker2/res/drawable/ic_add.xml
deleted file mode 100644
index 22b3fe9..0000000
--- a/packages/SoundPicker2/res/drawable/ic_add.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
- Copyright (C) 2016 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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24.0dp"
- android:height="24.0dp"
- android:viewportWidth="48.0"
- android:viewportHeight="48.0">
- <path
- android:fillColor="?android:attr/colorAccent"
- android:pathData="M38.0,26.0L26.0,26.0l0.0,12.0l-4.0,0.0L22.0,26.0L10.0,26.0l0.0,-4.0l12.0,0.0L22.0,10.0l4.0,0.0l0.0,12.0l12.0,0.0l0.0,4.0z"/>
-</vector>
\ No newline at end of file
diff --git a/packages/SoundPicker2/res/drawable/ic_add_padded.xml b/packages/SoundPicker2/res/drawable/ic_add_padded.xml
deleted file mode 100644
index c376867..0000000
--- a/packages/SoundPicker2/res/drawable/ic_add_padded.xml
+++ /dev/null
@@ -1,22 +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.
--->
-
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_add"
- android:insetTop="4dp"
- android:insetRight="4dp"
- android:insetBottom="4dp"
- android:insetLeft="4dp"/>
diff --git a/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml b/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml
deleted file mode 100644
index edfc0ab..0000000
--- a/packages/SoundPicker2/res/layout-watch/add_new_sound_item.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<!--
- Currently, no file manager app on watch could handle ACTION_GET_CONTENT intent.
- Make the visibility to "gone" to prevent failures.
- -->
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/add_new_sound_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:text="@null"
- android:textColor="?android:attr/colorAccent"
- android:gravity="center_vertical"
- android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
- android:drawableStart="@drawable/ic_add_padded"
- android:drawablePadding="8dp"
- android:ellipsize="marquee"
- android:visibility="gone" />
diff --git a/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml b/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml
deleted file mode 100644
index ee29a37..0000000
--- a/packages/SoundPicker2/res/layout-watch/radio_with_work_badge.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<com.android.soundpicker.CheckedListItem xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:background="?android:attr/selectableItemBackground"
- >
-
- <CheckedTextView
- android:id="@+id/checked_text_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="?android:attr/textColorAlertDialogListItem"
- android:gravity="center_vertical"
- android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
- android:drawableStart="?android:attr/listChoiceIndicatorSingle"
- android:drawablePadding="8dp"
- android:ellipsize="marquee"
- android:layout_toLeftOf="@+id/work_icon"
- android:maxLines="3" />
-
- <ImageView
- android:id="@id/work_icon"
- android:layout_width="18dp"
- android:layout_height="18dp"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:scaleType="centerCrop"
- android:layout_marginRight="20dp" />
-</com.android.soundpicker.CheckedListItem>
diff --git a/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml b/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml
deleted file mode 100644
index 6fc6080..0000000
--- a/packages/SoundPicker2/res/layout/activity_ringtone_picker.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?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.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/packages/SoundPicker2/res/layout/add_new_sound_item.xml b/packages/SoundPicker2/res/layout/add_new_sound_item.xml
deleted file mode 100644
index 024b97e..0000000
--- a/packages/SoundPicker2/res/layout/add_new_sound_item.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2016 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:background="?android:attr/selectableItemBackground"
- android:focusable="true"
- android:clickable="true">
-
- <ImageView
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:scaleType="centerCrop"
- android:layout_marginRight="24dp"
- android:layout_marginLeft="24dp"
- android:src="@drawable/ic_add"/>
-
- <TextView
- android:id="@+id/add_new_sound_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- android:text="@null"
- android:textColor="?android:attr/colorAccent"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:maxLines="3"
- android:gravity="center_vertical"
- android:paddingEnd="?android:attr/dialogPreferredPadding"
- android:drawablePadding="20dp"
- android:ellipsize="marquee"/>
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml b/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml
deleted file mode 100644
index 787f92e..0000000
--- a/packages/SoundPicker2/res/layout/fragment_ringtone_picker.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?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.
--->
-<androidx.recyclerview.widget.RecyclerView
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/recycler_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
-/>
\ No newline at end of file
diff --git a/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml b/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml
deleted file mode 100644
index 7efd911..0000000
--- a/packages/SoundPicker2/res/layout/fragment_tabbed_dialog.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?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.
--->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <com.google.android.material.tabs.TabLayout
- android:id="@+id/tabLayout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
- <androidx.viewpager2.widget.ViewPager2
- android:id="@+id/masterViewPager"
- android:paddingTop="12dp"
- android:paddingBottom="12dp"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/SoundPicker2/res/layout/radio_with_work_badge.xml b/packages/SoundPicker2/res/layout/radio_with_work_badge.xml
deleted file mode 100644
index 36ac93e..0000000
--- a/packages/SoundPicker2/res/layout/radio_with_work_badge.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<com.android.soundpicker.CheckedListItem
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:background="?android:attr/selectableItemBackground"
- android:focusable="true"
- android:clickable="true">
-
- <CheckedTextView
- android:id="@+id/checked_text_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?android:attr/listPreferredItemHeightSmall"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="?android:attr/textColorAlertDialogListItem"
- android:gravity="center_vertical"
- android:paddingStart="20dp"
- android:paddingEnd="?android:attr/dialogPreferredPadding"
- android:drawableStart="?android:attr/listChoiceIndicatorSingle"
- android:drawablePadding="20dp"
- android:ellipsize="marquee"
- android:layout_toLeftOf="@+id/work_icon"
- android:maxLines="3"/>
-
- <ImageView
- android:id="@id/work_icon"
- android:layout_width="18dp"
- android:layout_height="18dp"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:scaleType="centerCrop"
- android:layout_marginRight="20dp"/>
-</com.android.soundpicker.CheckedListItem>
diff --git a/packages/SoundPicker2/res/raw/default_alarm_alert.ogg b/packages/SoundPicker2/res/raw/default_alarm_alert.ogg
deleted file mode 100644
index e69de29..0000000
--- a/packages/SoundPicker2/res/raw/default_alarm_alert.ogg
+++ /dev/null
diff --git a/packages/SoundPicker2/res/raw/default_notification_sound.ogg b/packages/SoundPicker2/res/raw/default_notification_sound.ogg
deleted file mode 100644
index e69de29..0000000
--- a/packages/SoundPicker2/res/raw/default_notification_sound.ogg
+++ /dev/null
diff --git a/packages/SoundPicker2/res/raw/default_ringtone.ogg b/packages/SoundPicker2/res/raw/default_ringtone.ogg
deleted file mode 100644
index e69de29..0000000
--- a/packages/SoundPicker2/res/raw/default_ringtone.ogg
+++ /dev/null
diff --git a/packages/SoundPicker2/res/values/config.xml b/packages/SoundPicker2/res/values/config.xml
deleted file mode 100644
index 4e237a2..0000000
--- a/packages/SoundPicker2/res/values/config.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<!-- These resources are around just to allow their values to be customized
- for different hardware and product builds. Do not translate.
-
- NOTE: The naming convention is "config_camelCaseValue". -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <!-- True if the ringtone picker should show the ok/cancel buttons. If it is not shown, the
- ringtone will be automatically selected when the picker is closed. -->
- <bool name="config_showOkCancelButtons">true</bool>
-</resources>
diff --git a/packages/SoundPicker2/res/values/strings.xml b/packages/SoundPicker2/res/values/strings.xml
deleted file mode 100644
index ab7b95a..0000000
--- a/packages/SoundPicker2/res/values/strings.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2009 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <!-- Choice in the ringtone picker. If chosen, the default ringtone will be used. -->
- <string name="ringtone_default">Default ringtone</string>
-
- <!-- Choice in the notification sound picker. If chosen, the default notification sound will be
- used. -->
- <string name="notification_sound_default">Default notification sound</string>
-
- <!-- Choice in the alarm sound picker. If chosen, the default alarm sound will be used. -->
- <string name="alarm_sound_default">Default alarm sound</string>
-
- <!-- Text for the RingtonePicker item that allows adding a new ringtone. -->
- <string name="add_ringtone_text">Add ringtone</string>
- <!-- Text for the RingtonePicker item that allows adding a new alarm. -->
- <string name="add_alarm_text">Add alarm</string>
- <!-- Text for the RingtonePicker item that allows adding a new notification. -->
- <string name="add_notification_text">Add notification</string>
- <!-- Text for the RingtonePicker item ContextMenu that allows deleting a custom ringtone. -->
- <string name="delete_ringtone_text">Delete</string>
- <!-- Text for the Toast displayed when adding a custom ringtone fails. -->
- <string name="unable_to_add_ringtone">Unable to add custom ringtone</string>
- <!-- Text for the Toast displayed when deleting a custom ringtone fails. -->
- <string name="unable_to_delete_ringtone">Unable to delete custom ringtone</string>
-
- <!-- Text for the name of the app. [CHAR LIMIT=12] -->
- <string name="app_label">Sounds</string>
-
- <string name="empty_list">The list is empty</string>
- <string name="sound_page_title">Sound</string>
- <string name="vibration_page_title">Vibration</string>
-</resources>
diff --git a/packages/SoundPicker2/res/values/styles.xml b/packages/SoundPicker2/res/values/styles.xml
deleted file mode 100644
index d22d9c4..0000000
--- a/packages/SoundPicker2/res/values/styles.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <style name="PickerDialogTheme" parent="@*android:style/Theme.DeviceDefault.Settings.Dialog">
- </style>
-
-</resources>
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java
deleted file mode 100644
index 4fc2a86..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/BasePickerFragment.java
+++ /dev/null
@@ -1,312 +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.soundpicker;
-
-import android.app.Activity;
-import android.content.ContentProvider;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.provider.MediaStore;
-import android.util.Log;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import dagger.hilt.android.AndroidEntryPoint;
-
-import java.util.Objects;
-
-/**
- * Base class for generic picker fragments.
- *
- * <p>This fragment displays a recycler view that is populated by a {@link RingtoneListViewAdapter}
- * with data provided by a {@link RingtoneListHandler}. Each item can be selected on click,
- * which also triggers a ringtone preview performed by the shared {@link RingtonePickerViewModel}.
- * The ringtone preview uses the selection state of all picker fragments (e.g. sound selected by
- * one fragment and vibration selected by another).
- */
-@AndroidEntryPoint(Fragment.class)
-public abstract class BasePickerFragment extends Hilt_BasePickerFragment implements
- RingtoneListViewAdapter.Callbacks {
-
- private static final String TAG = "BasePickerFragment";
- private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE;
- private boolean mIsManagedProfile;
- private Drawable mWorkIconDrawable;
-
- protected RingtoneListViewAdapter mRingtoneListViewAdapter;
- protected RecyclerView mRecyclerView;
- protected RingtonePickerViewModel.Config mPickerConfig;
- protected RingtonePickerViewModel mRingtonePickerViewModel;
- protected RingtoneListHandler.Config mRingtoneListConfig;
- protected RingtoneListHandler mRingtoneListHandler;
-
- public BasePickerFragment() {
- super(R.layout.fragment_ringtone_picker);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
- RingtonePickerViewModel.class);
- mRingtoneListHandler = getRingtoneListHandler();
- mRecyclerView = view.requireViewById(R.id.recycler_view);
-
- mPickerConfig = mRingtonePickerViewModel.getPickerConfig();
- mRingtoneListConfig = mRingtoneListHandler.getRingtoneListConfig();
-
- mIsManagedProfile = UserManager.get(requireActivity()).isManagedProfile(
- mPickerConfig.userId);
-
- mRingtoneListViewAdapter = createRingtoneListViewAdapter();
- mRecyclerView.setHasFixedSize(true);
- mRecyclerView.setAdapter(mRingtoneListViewAdapter);
- mRecyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
- setSelectedItem(mRingtoneListHandler.getSelectedItemPosition());
- prepareRecyclerView(mRecyclerView);
- }
-
- @Override
- public boolean isWorkRingtone(int position) {
- if (!mIsManagedProfile) {
- return false;
- }
-
- /*
- * Display the work icon if the ringtone belongs to a work profile. We
- * can tell that a ringtone belongs to a work profile if the picker user
- * is a managed profile, the ringtone Uri is in external storage, and
- * either the uri has no user id or has the id of the picker user
- */
- Uri currentUri = mRingtoneListHandler.getRingtoneUri(position);
- int uriUserId = ContentProvider.getUserIdFromUri(currentUri,
- mPickerConfig.userId);
- Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri);
-
- return uriUserId == mPickerConfig.userId
- && uriWithoutUserId.toString().startsWith(
- MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString());
- }
-
- @Override
- public Drawable getWorkIconDrawable() {
- if (mWorkIconDrawable == null) {
- mWorkIconDrawable = requireActivity().getPackageManager()
- .getUserBadgeForDensityNoBackground(
- UserHandle.of(mPickerConfig.userId), /* density= */ -1);
- }
-
- return mWorkIconDrawable;
- }
-
- @Override
- public void onRingtoneSelected(int position) {
- setSelectedItem(position);
-
- // In the buttonless (watch-only) version, preemptively set our result since
- // we won't have another chance to do so before the activity closes.
- if (!mPickerConfig.showOkCancelButtons) {
- setSuccessResultWithSelectedRingtone();
- }
-
- // Play clip
- mRingtonePickerViewModel.playRingtone();
- }
-
- @Override
- public void onAddRingtoneSelected() {
- addRingtoneAsync();
- }
-
- /**
- * Sets up the list by adding fixed items to the top and bottom, if required. And sets the
- * selected item in the list.
- * @param recyclerView The recyclerview that contains the list of displayed items.
- */
- protected void prepareRecyclerView(@NonNull RecyclerView recyclerView) {
- // Reset the static item count, as this method can be called multiple times
- mRingtoneListHandler.resetFixedItems();
-
- if (mRingtoneListConfig.hasDefaultItem) {
- int defaultItemPos = addDefaultRingtoneItem();
-
- if (getSelectedItem() < 0
- && RingtoneManager.isDefault(mRingtoneListConfig.initialSelectedUri)) {
- setSelectedItem(defaultItemPos);
- }
- }
-
- if (mRingtoneListConfig.hasSilentItem) {
- int silentItemPos = addSilentItem();
-
- // The 'Silent' item should use a null Uri
- if (getSelectedItem() < 0
- && mRingtoneListConfig.initialSelectedUri == null) {
- setSelectedItem(silentItemPos);
- }
- }
-
- if (getSelectedItem() < 0) {
- setSelectedItem(mRingtoneListHandler.getRingtonePosition(
- mRingtoneListConfig.initialSelectedUri));
- }
-
- // In the buttonless (watch-only) version, preemptively set our result since we won't
- // have another chance to do so before the activity closes.
- if (!mPickerConfig.showOkCancelButtons) {
- setSuccessResultWithSelectedRingtone();
- }
-
- addNewRingtoneItem();
-
- // Enable context menu in ringtone items
- registerForContextMenu(recyclerView);
- }
-
- /**
- * Returns the fragment's sound/vibration list handler.
- * @return The ringtone list handler.
- */
- protected abstract RingtoneListHandler getRingtoneListHandler();
-
- /**
- * Starts the process to add a new ringtone to the list of ringtones asynchronously.
- * Currently, only works for adding sound files.
- */
- protected abstract void addRingtoneAsync();
-
- /**
- * Adds an item to the end of the list that can be used to add new ringtones to the list.
- * Currently, only works for adding sound files.
- */
- protected abstract void addNewRingtoneItem();
-
- protected int getSelectedItem() {
- return mRingtoneListHandler.getSelectedItemPosition();
- }
-
- /**
- * Returns the selected URI to the caller activity.
- */
- protected void setSuccessResultWithSelectedRingtone() {
- requireActivity().setResult(Activity.RESULT_OK,
- new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI,
- mRingtonePickerViewModel.getSelectedRingtoneUri()));
- }
-
- /**
- * Creates a ringtone recyclerview adapter using the ringtone manager cursor.
- * @return The created RingtoneListViewAdapter.
- */
- protected RingtoneListViewAdapter createRingtoneListViewAdapter() {
- LocalizedCursor cursor = new LocalizedCursor(
- mRingtoneListHandler.getRingtoneCursor(), getResources(), COLUMN_LABEL);
- return new RingtoneListViewAdapter(cursor, /* RingtoneListViewAdapterCallbacks= */ this);
- }
-
- /**
- * Sets the selected item in the list and scroll to the position in the recyclerview.
- * @param pos the position of the selected item in the list.
- */
- protected void setSelectedItem(int pos) {
- Objects.requireNonNull(mRingtoneListViewAdapter);
- mRingtoneListHandler.setSelectedItemPosition(pos);
- mRingtoneListViewAdapter.setSelectedItem(pos);
- mRingtoneListHandler.setSelectedItemId(mRingtoneListViewAdapter.getItemId(pos));
- mRecyclerView.scrollToPosition(pos);
- }
-
- /**
- * Adds a fixed item to the fixed items list . A fixed item is one that is not from
- * the RingtoneManager.
- *
- * @param textResId The resource ID of the text for the item.
- * @return The index of the inserted fixed item in the adapter.
- */
- protected int addFixedItem(int textResId) {
- return mRingtoneListViewAdapter.addTitleForFixedItem(textResId);
- }
-
- /**
- * Re-query RingtoneManager for the most recent set of installed ringtones. May move the
- * selected item position to match the new position of the chosen ringtone.
- * <p>
- * This should only need to happen after adding or removing a ringtone.
- */
- protected void requeryForAdapter() {
- mRingtonePickerViewModel.reinit();
- // Refresh and set a new cursor, and closing the old one.
- mRingtoneListViewAdapter = createRingtoneListViewAdapter();
- mRecyclerView.setAdapter(mRingtoneListViewAdapter);
- prepareRecyclerView(mRecyclerView);
-
- // Update selected item location.
- for (int i = 0; i < mRingtoneListViewAdapter.getItemCount(); i++) {
- if (mRingtoneListViewAdapter.getItemId(i)
- == mRingtoneListHandler.getSelectedItemId()) {
- setSelectedItem(i);
- return;
- }
- }
-
- // If selected item is still unknown, then set it to the default item, if available.
- // If it's not available, then attempt to set it to the silent item in the list.
- int selectedPosition = mRingtoneListHandler.getDefaultItemPosition();
-
- if (selectedPosition < 0) {
- selectedPosition = mRingtoneListHandler.getSilentItemPosition();
- }
-
- setSelectedItem(selectedPosition);
- }
-
- private int addDefaultRingtoneItem() {
- int defaultItemPosInAdapter = addFixedItem(
- RingtonePickerViewModel.getDefaultRingtoneItemTextByType(
- mPickerConfig.ringtoneType));
- int defaultItemPosInListHandler = mRingtoneListHandler.addDefaultItem();
-
- if (defaultItemPosInAdapter != defaultItemPosInListHandler) {
- Log.wtf(TAG, "Default item position in adapter and list handler must match.");
- return RingtoneListHandler.ITEM_POSITION_UNKNOWN;
- }
-
- return defaultItemPosInListHandler;
- }
-
- private int addSilentItem() {
- int silentItemPosInAdapter = addFixedItem(com.android.internal.R.string.ringtone_silent);
- int silentItemPosInListHandler = mRingtoneListHandler.addSilentItem();
-
- if (silentItemPosInAdapter != silentItemPosInListHandler) {
- Log.wtf(TAG, "Silent item position in adapter and list handler must match.");
- return RingtoneListHandler.ITEM_POSITION_UNKNOWN;
- }
-
- return silentItemPosInListHandler;
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java b/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java
deleted file mode 100644
index 819ae98..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/CheckedListItem.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2016 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.soundpicker;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.Checkable;
-import android.widget.CheckedTextView;
-import android.widget.RelativeLayout;
-
-/**
- * The {@link CheckedListItem} is a layout item that represents a ringtone, and is used in
- * {@link RingtonePickerActivity}. It contains the ringtone's name, and a work badge to right of the
- * name if the ringtone belongs to a work profile.
- */
-public class CheckedListItem extends RelativeLayout implements Checkable {
-
- public CheckedListItem(Context context) {
- super(context);
- }
-
- public CheckedListItem(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public CheckedListItem(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public CheckedListItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- public void setChecked(boolean checked) {
- getCheckedTextView().setChecked(checked);
- }
-
- @Override
- public boolean isChecked() {
- return getCheckedTextView().isChecked();
- }
-
- @Override
- public void toggle() {
- getCheckedTextView().toggle();
- }
-
- private CheckedTextView getCheckedTextView() {
- return (CheckedTextView) findViewById(R.id.checked_text_view);
- }
-
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java
deleted file mode 100644
index afdbf05..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/ListeningExecutorServiceFactory.java
+++ /dev/null
@@ -1,44 +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.soundpicker;
-
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-
-import java.util.concurrent.Executors;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-/**
- * A factory class used to create {@link ListeningExecutorService}.
- */
-@Singleton
-public class ListeningExecutorServiceFactory {
-
- @Inject
- ListeningExecutorServiceFactory() {
- }
-
- /**
- * Returns a single thread {@link ListeningExecutorService}.
- *
- */
- public ListeningExecutorService createSingleThreadExecutor() {
- return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java b/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java
deleted file mode 100644
index 83d04a3..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/LocalizedCursor.java
+++ /dev/null
@@ -1,117 +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.soundpicker;
-
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.CursorWrapper;
-import android.util.Log;
-import android.util.TypedValue;
-
-import androidx.annotation.Nullable;
-
-import java.util.Locale;
-import java.util.regex.Pattern;
-
-/**
- * A cursor wrapper class mainly used to guarantee getting a ringtone title
- */
-final class LocalizedCursor extends CursorWrapper {
-
- private static final String TAG = "LocalizedCursor";
- private static final String SOUND_NAME_RES_PREFIX = "sound_name_";
-
- private final int mTitleIndex;
- private final Resources mResources;
- private final Pattern mSanitizePattern;
- private final String mNamePrefix;
-
- LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) {
- super(cursor);
- mTitleIndex = mCursor.getColumnIndex(columnLabel);
- mResources = resources;
- mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]");
- if (mTitleIndex == -1) {
- Log.e(TAG, "No index for column " + columnLabel);
- mNamePrefix = null;
- } else {
- mNamePrefix = buildNamePrefix(mResources);
- }
- }
-
- /**
- * Builds the prefix for the name of the resource to look up.
- * The format is: "ResourcePackageName::ResourceTypeName/" (the type name is expected to be
- * "string" but let's not hardcode it).
- * Here we use an existing resource "notification_sound_default" which is always expected to be
- * found.
- *
- * @param resources Application's resources
- * @return the built name prefix, or null if failed to build.
- */
- @Nullable
- private static String buildNamePrefix(Resources resources) {
- try {
- return String.format("%s:%s/%s",
- resources.getResourcePackageName(R.string.notification_sound_default),
- resources.getResourceTypeName(R.string.notification_sound_default),
- SOUND_NAME_RES_PREFIX);
- } catch (Resources.NotFoundException e) {
- Log.e(TAG, "Failed to build the prefix for the name of the resource.", e);
- }
-
- return null;
- }
-
- /**
- * Process resource name to generate a valid resource name.
- *
- * @return a non-null String
- */
- private String sanitize(String input) {
- if (input == null) {
- return "";
- }
- return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase(Locale.ROOT);
- }
-
- @Override
- public String getString(int columnIndex) {
- final String defaultName = mCursor.getString(columnIndex);
- if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) {
- return defaultName;
- }
- TypedValue value = new TypedValue();
- try {
- // the name currently in the database is used to derive a name to match
- // against resource names in this package
- mResources.getValue(mNamePrefix + sanitize(defaultName), value,
- /* resolveRefs= */ false);
- } catch (Resources.NotFoundException e) {
- Log.d(TAG, "Failed to get localized string. Using default string instead.", e);
- return defaultName;
- }
- if ((value != null) && (value.type == TypedValue.TYPE_STRING)) {
- Log.d(TAG, String.format("Replacing name %s with %s",
- defaultName, value.string.toString()));
- return value.string.toString();
- } else {
- Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName);
- return defaultName;
- }
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java
deleted file mode 100644
index 6817f53..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneFactory.java
+++ /dev/null
@@ -1,60 +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.soundpicker;
-
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-
-import dagger.hilt.android.qualifiers.ApplicationContext;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-/**
- * A factory class used to create {@link Ringtone}.
- */
-@Singleton
-public class RingtoneFactory {
-
- private final Context mApplicationContext;
-
- @Inject
- RingtoneFactory(@ApplicationContext Context applicationContext) {
- mApplicationContext = applicationContext;
- }
-
- /**
- * Returns a {@link Ringtone} built from the provided URI and audio attributes flags.
- *
- * @param uri The URI used to build the {@link Ringtone}.
- * @param audioAttributesFlags A combination of audio attribute flags that affect the volume
- * and settings when playing the ringtone.
- * @return the built {@link Ringtone}.
- */
- public Ringtone create(Uri uri, int audioAttributesFlags) {
- AudioAttributes audioAttributes = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .setFlags(audioAttributesFlags)
- .build();
- return RingtoneManager.getRingtone(mApplicationContext, uri,
- /* volumeShaperConfig= */ null, audioAttributes);
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java
deleted file mode 100644
index bb38e0e..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListHandler.java
+++ /dev/null
@@ -1,222 +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.soundpicker;
-
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.Nullable;
-import android.database.Cursor;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import javax.inject.Inject;
-
-/**
- * Handles ringtone list state and actions. This includes keeping track of the selected item,
- * ringtone manager cursor and added items to the list.
- */
-public class RingtoneListHandler {
-
- // TODO: We're using an empty URI instead of null, because null URIs still produce a sound,
- // while empty ones don't (Potentially this might be due to empty URIs being perceived as
- // malformed ones). We will switch to using the official silent URIs (SOUND_OFF, VIBRATION_OFF)
- // once they become available.
- static final Uri SILENT_URI = Uri.EMPTY;
- static final int ITEM_POSITION_UNKNOWN = -1;
-
- private static final String TAG = "RingtoneListHandler";
-
- /** The position in the list of the 'Silent' item. */
- private int mSilentItemPosition = ITEM_POSITION_UNKNOWN;
- /** The position in the list of the 'Default' item. */
- private int mDefaultItemPosition = ITEM_POSITION_UNKNOWN;
- /** The number of fixed items in the list. */
- private int mFixedItemCount;
- /**
- * Stable ID for the ringtone that is currently selected (may be -1 if no ringtone is selected).
- */
- private long mSelectedItemId = -1;
- private int mSelectedItemPosition = ITEM_POSITION_UNKNOWN;
-
- private RingtoneManager mRingtoneManager;
- private Config mRingtoneListConfig;
- private Cursor mRingtoneCursor;
-
- /**
- * Holds immutable info on the ringtone list that is displayed.
- */
- static final class Config {
- /**
- * Whether this list has the 'Default' item.
- */
- public final boolean hasDefaultItem;
- /**
- * The Uri to play when the 'Default' item is clicked.
- */
- public final Uri uriForDefaultItem;
- /**
- * Whether this list has the 'Silent' item.
- */
- public final boolean hasSilentItem;
- /**
- * The initially selected uri in the list.
- */
- public final Uri initialSelectedUri;
-
- Config(boolean hasDefaultItem, Uri uriForDefaultItem, boolean hasSilentItem,
- Uri initialSelectedUri) {
- this.hasDefaultItem = hasDefaultItem;
- this.uriForDefaultItem = uriForDefaultItem;
- this.hasSilentItem = hasSilentItem;
- this.initialSelectedUri = initialSelectedUri;
- }
- }
-
- @Inject
- RingtoneListHandler() {
- }
-
- void init(@NonNull Config ringtoneListConfig,
- @NonNull RingtoneManager ringtoneManager, @NonNull Cursor ringtoneCursor) {
- mRingtoneManager = requireNonNull(ringtoneManager);
- mRingtoneListConfig = requireNonNull(ringtoneListConfig);
- mRingtoneCursor = requireNonNull(ringtoneCursor);
- }
-
- Config getRingtoneListConfig() {
- return mRingtoneListConfig;
- }
-
- Cursor getRingtoneCursor() {
- requireInitCalled();
- return mRingtoneCursor;
- }
-
- Uri getRingtoneUri(int position) {
- if (position < 0) {
- Log.w(TAG, "Selected item position is unknown.");
- // When the selected item is ITEM_POSITION_UNKNOWN, it is not the case we expected.
- // We return SILENT_URI for this case.
- return SILENT_URI;
- } else if (position == mDefaultItemPosition) {
- // Use the default Uri that they originally gave us.
- return mRingtoneListConfig.uriForDefaultItem;
- } else if (position == mSilentItemPosition) {
- // Use SILENT_URI for the 'Silent' item.
- return SILENT_URI;
- } else {
- requireInitCalled();
- return mRingtoneManager.getRingtoneUri(mapListPositionToRingtonePosition(position));
- }
- }
-
- int getRingtonePosition(Uri uri) {
- requireInitCalled();
- return mapRingtonePositionToListPosition(mRingtoneManager.getRingtonePosition(uri));
- }
-
- void resetFixedItems() {
- mFixedItemCount = 0;
- mDefaultItemPosition = ITEM_POSITION_UNKNOWN;
- mSilentItemPosition = ITEM_POSITION_UNKNOWN;
- }
-
- int addDefaultItem() {
- if (mDefaultItemPosition < 0) {
- mDefaultItemPosition = addFixedItem();
- }
- return mDefaultItemPosition;
- }
-
- int getDefaultItemPosition() {
- return mDefaultItemPosition;
- }
-
- int addSilentItem() {
- if (mSilentItemPosition < 0) {
- mSilentItemPosition = addFixedItem();
- }
- return mSilentItemPosition;
- }
-
- public int getSilentItemPosition() {
- return mSilentItemPosition;
- }
-
- int getSelectedItemPosition() {
- return mSelectedItemPosition;
- }
-
- void setSelectedItemPosition(int selectedItemPosition) {
- mSelectedItemPosition = selectedItemPosition;
- }
-
- void setSelectedItemId(long selectedItemId) {
- mSelectedItemId = selectedItemId;
- }
-
- long getSelectedItemId() {
- return mSelectedItemId;
- }
-
- @Nullable
- Uri getSelectedRingtoneUri() {
- return getRingtoneUri(mSelectedItemPosition);
- }
-
- /**
- * Maps the item position in the list, to its equivalent position in the RingtoneManager.
- *
- * @param itemPosition the position of item in the list.
- * @return position of the item in the RingtoneManager.
- */
- private int mapListPositionToRingtonePosition(int itemPosition) {
- // If the manager position is less than add items, then return that.
- if (itemPosition < mFixedItemCount) return itemPosition;
-
- return itemPosition - mFixedItemCount;
- }
-
- /**
- * Maps the item position in the RingtoneManager, to its equivalent position in the list.
- *
- * @param itemPosition the position of the item in the RingtoneManager.
- * @return position of the item in the list.
- */
- private int mapRingtonePositionToListPosition(int itemPosition) {
- // If the manager position is less than add items, then return that.
- if (itemPosition < 0) return itemPosition;
-
- return itemPosition + mFixedItemCount;
- }
-
- /**
- * Increments the number of added fixed items and returns the index of the newest added item.
- * @return index of the newest added fixed item.
- */
- private int addFixedItem() {
- return mFixedItemCount++;
- }
-
- private void requireInitCalled() {
- requireNonNull(mRingtoneManager);
- requireNonNull(mRingtoneCursor);
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java
deleted file mode 100644
index 4ca8943..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneListViewAdapter.java
+++ /dev/null
@@ -1,264 +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.soundpicker;
-
-import static com.android.internal.widget.RecyclerView.NO_ID;
-
-import android.database.Cursor;
-import android.graphics.drawable.Drawable;
-import android.media.RingtoneManager;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckedTextView;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.StringRes;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * The adapter presents a list of ringtones which may include fixed item in the list and an action
- * button at the end.
- *
- * The adapter handles three different types of items:
- * <ul>
- * <li>FIXED: Fixed items are items added to the top of the list. These items can not be modified
- * and their position will never change.
- * <li>DYNAMIC: Dynamic items are items from the ringtone manager. These items can be modified
- * and their position can change.
- * <li>FOOTER: A footer item is an added button to the end of the list. This item can be clicked
- * but not selected and its position will never change.
- * </ul>
- */
-final class RingtoneListViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
-
- private static final int VIEW_TYPE_FIXED_ITEM = 0;
- private static final int VIEW_TYPE_DYNAMIC_ITEM = 1;
- private static final int VIEW_TYPE_ADD_RINGTONE_ITEM = 2;
- private final Cursor mCursor;
- private final List<Integer> mFixedItemTitles;
- private final Callbacks mCallbacks;
- private final int mRowIDColumn;
- private int mSelectedItem = -1;
- @StringRes private Integer mAddRingtoneItemTitle;
-
- /** Provides callbacks for the adapter. */
- interface Callbacks {
- void onRingtoneSelected(int position);
- void onAddRingtoneSelected();
- boolean isWorkRingtone(int position);
- Drawable getWorkIconDrawable();
- }
-
- RingtoneListViewAdapter(Cursor cursor,
- Callbacks callbacks) {
- mCursor = cursor;
- mCallbacks = callbacks;
- mFixedItemTitles = new ArrayList<>();
- mRowIDColumn = mCursor != null ? mCursor.getColumnIndex("_id") : -1;
- setHasStableIds(true);
- }
-
- void setSelectedItem(int position) {
- notifyItemChanged(mSelectedItem);
- mSelectedItem = position;
- notifyItemChanged(mSelectedItem);
- }
-
- /**
- * Adds title to the fixed items list and returns the index of the newest added item.
- * @param textResId the title to add to the fixed items list.
- * @return The index of the newest added item in the fixed items list.
- */
- int addTitleForFixedItem(@StringRes int textResId) {
- mFixedItemTitles.add(textResId);
- notifyItemInserted(mFixedItemTitles.size() - 1);
- return mFixedItemTitles.size() - 1;
- }
-
- void addTitleForAddRingtoneItem(@StringRes int textResId) {
- mAddRingtoneItemTitle = textResId;
- notifyItemInserted(getItemCount() - 1);
- }
-
- @NotNull
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- LayoutInflater inflater = LayoutInflater.from(parent.getContext());
-
- if (viewType == VIEW_TYPE_FIXED_ITEM) {
- View fixedItemView = inflater.inflate(
- com.android.internal.R.layout.select_dialog_singlechoice_material, parent,
- false);
-
- return new FixedItemViewHolder(fixedItemView, mCallbacks);
- }
-
- if (viewType == VIEW_TYPE_ADD_RINGTONE_ITEM) {
- View addRingtoneItemView = inflater.inflate(R.layout.add_new_sound_item, parent, false);
-
- return new AddRingtoneItemViewHolder(addRingtoneItemView,
- mCallbacks);
- }
-
- View view = inflater.inflate(R.layout.radio_with_work_badge, parent, false);
-
- return new DynamicItemViewHolder(view, mCallbacks);
- }
-
- @Override
- public void onBindViewHolder(@NotNull RecyclerView.ViewHolder holder, int position) {
- if (holder instanceof FixedItemViewHolder) {
- FixedItemViewHolder viewHolder = (FixedItemViewHolder) holder;
-
- viewHolder.onBind(mFixedItemTitles.get(position),
- /* isChecked= */ position == mSelectedItem);
- return;
- }
- if (holder instanceof AddRingtoneItemViewHolder) {
- AddRingtoneItemViewHolder viewHolder = (AddRingtoneItemViewHolder) holder;
-
- viewHolder.onBind(mAddRingtoneItemTitle);
- return;
- }
-
- if (!(holder instanceof DynamicItemViewHolder)) {
- throw new IllegalArgumentException("holder type is not supported");
- }
-
- DynamicItemViewHolder viewHolder = (DynamicItemViewHolder) holder;
- int pos = position - mFixedItemTitles.size();
- if (!mCursor.moveToPosition(pos)) {
- throw new IllegalStateException("Could not move cursor to position: " + pos);
- }
-
- Drawable workIcon = (mCallbacks != null)
- && mCallbacks.isWorkRingtone(position)
- ? mCallbacks.getWorkIconDrawable() : null;
-
- viewHolder.onBind(mCursor.getString(RingtoneManager.TITLE_COLUMN_INDEX),
- /* isChecked= */ position == mSelectedItem, workIcon);
- }
-
- @Override
- public int getItemViewType(int position) {
- if (!mFixedItemTitles.isEmpty() && position < mFixedItemTitles.size()) {
- return VIEW_TYPE_FIXED_ITEM;
- }
- if (mAddRingtoneItemTitle != null && position == getItemCount() - 1) {
- return VIEW_TYPE_ADD_RINGTONE_ITEM;
- }
-
- return VIEW_TYPE_DYNAMIC_ITEM;
- }
-
- @Override
- public int getItemCount() {
- int itemCount = mFixedItemTitles.size() + mCursor.getCount();
-
- if (mAddRingtoneItemTitle != null) {
- itemCount++;
- }
-
- return itemCount;
- }
-
- @Override
- public long getItemId(int position) {
- int itemViewType = getItemViewType(position);
- if (itemViewType == VIEW_TYPE_FIXED_ITEM) {
- // Since the item is a fixed item, then we can use the position as a stable ID
- // since the order of the fixed items should never change.
- return position;
- }
- if (itemViewType == VIEW_TYPE_DYNAMIC_ITEM && mCursor != null
- && mCursor.moveToPosition(position - mFixedItemTitles.size())
- && mRowIDColumn != -1) {
- return mCursor.getLong(mRowIDColumn) + mFixedItemTitles.size();
- }
-
- // The position is either invalid or the item is the add ringtone item view, so no stable
- // ID is returned. Add ringtone item view cannot be selected and only include an action
- // buttons.
- return NO_ID;
- }
-
- private static class DynamicItemViewHolder extends RecyclerView.ViewHolder {
- private final CheckedTextView mTitleTextView;
- private final ImageView mWorkIcon;
-
- DynamicItemViewHolder(View itemView, Callbacks listener) {
- super(itemView);
-
- mTitleTextView = itemView.requireViewById(R.id.checked_text_view);
- mWorkIcon = itemView.requireViewById(R.id.work_icon);
- itemView.setOnClickListener(v -> listener.onRingtoneSelected(this.getLayoutPosition()));
- }
-
- void onBind(String title, boolean isChecked, Drawable workIcon) {
- mTitleTextView.setText(title);
- mTitleTextView.setChecked(isChecked);
-
- if (workIcon == null) {
- mWorkIcon.setVisibility(View.GONE);
- } else {
- mWorkIcon.setImageDrawable(workIcon);
- mWorkIcon.setVisibility(View.VISIBLE);
- }
- }
- }
-
- private static class FixedItemViewHolder extends RecyclerView.ViewHolder {
- private final CheckedTextView mTitleTextView;
-
- FixedItemViewHolder(View itemView, Callbacks listener) {
- super(itemView);
-
- mTitleTextView = (CheckedTextView) itemView;
- itemView.setOnClickListener(v -> listener.onRingtoneSelected(this.getLayoutPosition()));
- }
-
- void onBind(@StringRes int title, boolean isChecked) {
- Objects.requireNonNull(mTitleTextView);
-
- mTitleTextView.setText(title);
- mTitleTextView.setChecked(isChecked);
- }
- }
-
- private static class AddRingtoneItemViewHolder extends RecyclerView.ViewHolder {
- private final TextView mTitleTextView;
-
- AddRingtoneItemViewHolder(View itemView, Callbacks listener) {
- super(itemView);
-
- mTitleTextView = itemView.requireViewById(R.id.add_new_sound_text);
- itemView.setOnClickListener(v -> listener.onAddRingtoneSelected());
- }
-
- void onBind(@StringRes int title) {
- mTitleTextView.setText(title);
- }
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java
deleted file mode 100644
index f08eb24..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneManagerFactory.java
+++ /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.soundpicker;
-
-import android.content.Context;
-import android.media.RingtoneManager;
-
-import dagger.hilt.android.qualifiers.ApplicationContext;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-/**
- * A factory class used to create {@link RingtoneManager}.
- */
-@Singleton
-public class RingtoneManagerFactory {
-
- private final Context mApplicationContext;
-
- @Inject
- RingtoneManagerFactory(@ApplicationContext Context applicationContext) {
- mApplicationContext = applicationContext;
- }
-
- /**
- * Creates a new {@link RingtoneManager} and returns it.
- *
- * @return a {@link RingtoneManager}
- */
- public RingtoneManager create() {
- return new RingtoneManager(mApplicationContext, /* includeParentRingtones */ true);
- }
-}
-
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java
deleted file mode 100644
index b94ebeb..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneOverlayService.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.soundpicker;
-
-import android.app.Service;
-import android.content.Intent;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Environment;
-import android.os.FileUtils;
-import android.os.IBinder;
-import android.provider.MediaStore;
-import android.provider.Settings.System;
-import android.util.Log;
-
-import androidx.annotation.IdRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Service to copy and set customization of default sounds
- */
-public class RingtoneOverlayService extends Service {
- private static final String TAG = "RingtoneOverlayService";
- private static final boolean DEBUG = false;
-
- @Override
- public int onStartCommand(@Nullable final Intent intent, final int flags, final int startId) {
- AsyncTask.execute(() -> {
- updateRingtones();
- stopSelf();
- });
-
- // Try again later if we are killed before we finish.
- return Service.START_REDELIVER_INTENT;
- }
-
- @Override
- public IBinder onBind(@Nullable final Intent intent) {
- return null;
- }
-
- private void updateRingtones() {
- copyResourceAndSetAsSound(R.raw.default_ringtone,
- System.RINGTONE, Environment.DIRECTORY_RINGTONES);
- copyResourceAndSetAsSound(R.raw.default_notification_sound,
- System.NOTIFICATION_SOUND, Environment.DIRECTORY_NOTIFICATIONS);
- copyResourceAndSetAsSound(R.raw.default_alarm_alert,
- System.ALARM_ALERT, Environment.DIRECTORY_ALARMS);
- }
-
- /* If the resource contains any data, copy a resource to the file system, scan it, and set the
- * file URI as the default for a sound. */
- private void copyResourceAndSetAsSound(@IdRes final int id, @NonNull final String name,
- @NonNull final String subPath) {
- final File destDir = Environment.getExternalStoragePublicDirectory(subPath);
- if (!destDir.exists() && !destDir.mkdirs()) {
- Log.e(TAG, "can't create " + destDir.getAbsolutePath());
- return;
- }
-
- final File dest = new File(destDir, "default_" + name + ".ogg");
- try (
- InputStream is = getResources().openRawResource(id);
- FileOutputStream os = new FileOutputStream(dest);
- ) {
- if (is.available() > 0) {
- FileUtils.copy(is, os);
- final Uri uri = scanFile(dest);
- if (uri != null) {
- set(name, uri);
- }
- } else {
- // TODO Shall we remove any former copied resource in this case and unset
- // the defaults if we use this event a second time to clear the data?
- if (DEBUG) Log.d(TAG, "Resource for " + name + " has no overlay");
- }
- } catch (IOException e) {
- Log.e(TAG, "Unable to open resource for " + name + ": " + e);
- }
- }
-
- private Uri scanFile(@NonNull final File file) {
- return MediaStore.scanFile(getContentResolver(), file);
- }
-
- private void set(@NonNull final String name, @NonNull final Uri uri) {
- final Uri settingUri = System.getUriFor(name);
- RingtoneManager.setActualDefaultRingtoneUri(this,
- RingtoneManager.getDefaultType(settingUri), uri);
- System.putInt(getContentResolver(), name + "_set", 1);
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java
deleted file mode 100644
index 90a14f9..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerActivity.java
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Copyright (C) 2007 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.soundpicker;
-
-import android.content.Intent;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.Log;
-
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentTransaction;
-import androidx.lifecycle.ViewModelProvider;
-
-import dagger.hilt.android.AndroidEntryPoint;
-
-/**
- * The {@link RingtonePickerActivity} allows the user to choose one from all of the
- * available ringtones. The chosen ringtone's URI will be persisted as a string.
- *
- * @see RingtoneManager#ACTION_RINGTONE_PICKER
- */
-@AndroidEntryPoint(AppCompatActivity.class)
-public final class RingtonePickerActivity extends Hilt_RingtonePickerActivity {
-
- private static final String TAG = "RingtonePickerActivity";
- // TODO: Use the extra keys from RingtoneManager once they're added.
- private static final String EXTRA_RINGTONE_PICKER_CATEGORY = "EXTRA_RINGTONE_PICKER_CATEGORY";
- private static final String EXTRA_VIBRATION_SHOW_DEFAULT = "EXTRA_VIBRATION_SHOW_DEFAULT";
- private static final String EXTRA_VIBRATION_DEFAULT_URI = "EXTRA_VIBRATION_DEFAULT_URI";
- private static final String EXTRA_VIBRATION_SHOW_SILENT = "EXTRA_VIBRATION_SHOW_SILENT";
- private static final String EXTRA_VIBRATION_EXISTING_URI = "EXTRA_VIBRATION_EXISTING_URI";
- private static final boolean RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED = false;
-
- private RingtonePickerViewModel mRingtonePickerViewModel;
- private int mAttributesFlags;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_ringtone_picker);
-
- mRingtonePickerViewModel = new ViewModelProvider(this).get(RingtonePickerViewModel.class);
-
- Intent intent = getIntent();
- /**
- * Id of the user to which the ringtone picker should list the ringtones
- */
- int pickerUserId = UserHandle.myUserId();
-
- // Get the types of ringtones to show
- int ringtoneType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE,
- RingtonePickerViewModel.RINGTONE_TYPE_UNKNOWN);
-
- // AudioAttributes flags
- mAttributesFlags |= intent.getIntExtra(
- RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS,
- 0 /*defaultValue == no flags*/);
-
- boolean showOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons);
-
- String title = intent.getStringExtra(RingtoneManager.EXTRA_RINGTONE_TITLE);
- if (title == null) {
- title = getString(RingtonePickerViewModel.getTitleByType(ringtoneType));
- }
- String ringtonePickerCategory = intent.getStringExtra(EXTRA_RINGTONE_PICKER_CATEGORY);
- RingtonePickerViewModel.PickerType pickerType = mapCategoryToPickerType(
- ringtonePickerCategory);
-
- RingtoneListHandler.Config soundListConfig = getSoundListConfig(pickerType, intent,
- ringtoneType);
- RingtoneListHandler.Config vibrationListConfig = getVibrationListConfig(pickerType, intent);
-
- RingtonePickerViewModel.Config pickerConfig =
- new RingtonePickerViewModel.Config(title, pickerUserId, ringtoneType,
- showOkCancelButtons, mAttributesFlags, pickerType);
-
- mRingtonePickerViewModel.init(pickerConfig, soundListConfig, vibrationListConfig);
-
- if (savedInstanceState == null) {
- TabbedDialogFragment dialogFragment = new TabbedDialogFragment();
-
- FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
- Fragment prev = getSupportFragmentManager().findFragmentByTag(TabbedDialogFragment.TAG);
- if (prev != null) {
- ft.remove(prev);
- }
- ft.addToBackStack(null);
- dialogFragment.show(ft, TabbedDialogFragment.TAG);
- }
-
- // The volume keys will control the stream that we are choosing a ringtone for
- setVolumeControlStream(mRingtonePickerViewModel.getRingtoneStreamType());
- }
-
- private RingtoneListHandler.Config getSoundListConfig(
- RingtonePickerViewModel.PickerType pickerType, Intent intent, int ringtoneType) {
- if (pickerType != RingtonePickerViewModel.PickerType.SOUND_PICKER
- && pickerType != RingtonePickerViewModel.PickerType.RINGTONE_PICKER) {
- // This ringtone picker does not require a sound picker.
- return null;
- }
-
- // Get whether to show the 'Default' sound item, and the URI to play when it's clicked
- boolean hasDefaultSoundItem =
- intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
-
- // The Uri to play when the 'Default' sound item is clicked.
- Uri uriForDefaultSoundItem =
- intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI);
- if (uriForDefaultSoundItem == null) {
- uriForDefaultSoundItem = RingtonePickerViewModel.getDefaultItemUriByType(ringtoneType);
- }
-
- // Get whether this list has the 'Silent' sound item.
- boolean hasSilentSoundItem =
- intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
-
- // AudioAttributes flags
- mAttributesFlags |= intent.getIntExtra(
- RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS,
- 0 /*defaultValue == no flags*/);
-
- // Get the sound URI whose list item should have a checkmark
- Uri existingSoundUri = intent
- .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
-
- return new RingtoneListHandler.Config(hasDefaultSoundItem,
- uriForDefaultSoundItem, hasSilentSoundItem, existingSoundUri);
- }
-
- private RingtoneListHandler.Config getVibrationListConfig(
- RingtonePickerViewModel.PickerType pickerType, Intent intent) {
- if (pickerType != RingtonePickerViewModel.PickerType.VIBRATION_PICKER
- && pickerType != RingtonePickerViewModel.PickerType.RINGTONE_PICKER) {
- // This ringtone picker does not require a vibration picker.
- return null;
- }
-
- // Get whether to show the 'Default' vibration item, and the URI to play when it's clicked
- boolean hasDefaultVibrationItem =
- intent.getBooleanExtra(EXTRA_VIBRATION_SHOW_DEFAULT, false);
-
- // The Uri to play when the 'Default' vibration item is clicked.
- Uri uriForDefaultVibrationItem = intent.getParcelableExtra(EXTRA_VIBRATION_DEFAULT_URI);
-
- // Get whether this list has the 'Silent' vibration item.
- boolean hasSilentVibrationItem =
- intent.getBooleanExtra(EXTRA_VIBRATION_SHOW_SILENT, true);
-
- // Get the vibration URI whose list item should have a checkmark
- Uri existingVibrationUri = intent.getParcelableExtra(EXTRA_VIBRATION_EXISTING_URI);
-
- return new RingtoneListHandler.Config(
- hasDefaultVibrationItem, uriForDefaultVibrationItem, hasSilentVibrationItem,
- existingVibrationUri);
- }
-
- @Override
- public void onDestroy() {
- mRingtonePickerViewModel.cancelPendingAsyncTasks();
- super.onDestroy();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- mRingtonePickerViewModel.onStop(isChangingConfigurations());
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- mRingtonePickerViewModel.onPause(isChangingConfigurations());
- }
-
- /**
- * Maps the ringtone picker category to the appropriate PickerType.
- * If the category is null or the feature is still not released, then it defaults to sound
- * picker.
- *
- * @param category the ringtone picker category.
- * @return the corresponding picker type.
- */
- private static RingtonePickerViewModel.PickerType mapCategoryToPickerType(String category) {
- if (category == null || !RINGTONE_PICKER_CATEGORY_FEATURE_ENABLED) {
- return RingtonePickerViewModel.PickerType.SOUND_PICKER;
- }
-
- switch (category) {
- case "android.intent.category.RINGTONE_PICKER_RINGTONE":
- return RingtonePickerViewModel.PickerType.RINGTONE_PICKER;
- case "android.intent.category.RINGTONE_PICKER_SOUND":
- return RingtonePickerViewModel.PickerType.SOUND_PICKER;
- case "android.intent.category.RINGTONE_PICKER_VIBRATION":
- return RingtonePickerViewModel.PickerType.VIBRATION_PICKER;
- default:
- Log.w(TAG, "Unrecognized category: " + category + ". Defaulting to sound picker.");
- return RingtonePickerViewModel.PickerType.SOUND_PICKER;
- }
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java
deleted file mode 100644
index 2c09711..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerViewModel.java
+++ /dev/null
@@ -1,340 +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.soundpicker;
-
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.Nullable;
-import android.annotation.StringRes;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.provider.Settings;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.ViewModel;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-
-import dagger.hilt.android.lifecycle.HiltViewModel;
-
-import java.io.IOException;
-import java.util.concurrent.Executor;
-
-import javax.inject.Inject;
-
-/**
- * A view model which holds immutable info about the picker state and means to retrieve and play
- * currently selected ringtones.
- */
-@HiltViewModel
-public final class RingtonePickerViewModel extends ViewModel {
-
- static final int RINGTONE_TYPE_UNKNOWN = -1;
-
- /**
- * Keep the currently playing ringtone around when changing orientation, so that it
- * can be stopped later, after the activity is recreated.
- */
- @VisibleForTesting
- static Ringtone sPlayingRingtone;
-
- private static final String TAG = "RingtonePickerViewModel";
-
- private final RingtoneManagerFactory mRingtoneManagerFactory;
- private final RingtoneFactory mRingtoneFactory;
- private final RingtoneListHandler mSoundListHandler;
- private final RingtoneListHandler mVibrationListHandler;
- private final ListeningExecutorService mListeningExecutorService;
-
- private RingtoneManager mRingtoneManager;
-
- /**
- * The ringtone that's currently playing.
- */
- private Ringtone mCurrentRingtone;
-
- private Config mPickerConfig;
-
- private ListenableFuture<Uri> mAddCustomRingtoneFuture;
-
- public enum PickerType {
- RINGTONE_PICKER,
- SOUND_PICKER,
- VIBRATION_PICKER
- }
-
- /**
- * Holds immutable info on the picker that should be displayed.
- */
- static final class Config {
- public final String title;
- /**
- * Id of the user to which the ringtone picker should list the ringtones.
- */
- public final int userId;
- /**
- * Ringtone type.
- */
- public final int ringtoneType;
- /**
- * AudioAttributes flags.
- */
- public final int audioAttributesFlags;
- /**
- * In the buttonless (watch-only) version we don't show the OK/Cancel buttons.
- */
- public final boolean showOkCancelButtons;
-
- public final PickerType mPickerType;
-
- Config(String title, int userId, int ringtoneType, boolean showOkCancelButtons,
- int audioAttributesFlags, PickerType pickerType) {
- this.title = title;
- this.userId = userId;
- this.ringtoneType = ringtoneType;
- this.showOkCancelButtons = showOkCancelButtons;
- this.audioAttributesFlags = audioAttributesFlags;
- this.mPickerType = pickerType;
- }
- }
-
- @Inject
- RingtonePickerViewModel(RingtoneManagerFactory ringtoneManagerFactory,
- RingtoneFactory ringtoneFactory,
- ListeningExecutorServiceFactory listeningExecutorServiceFactory,
- RingtoneListHandler soundListHandler,
- RingtoneListHandler vibrationListHandler) {
- mRingtoneManagerFactory = ringtoneManagerFactory;
- mRingtoneFactory = ringtoneFactory;
- mListeningExecutorService = listeningExecutorServiceFactory.createSingleThreadExecutor();
- mSoundListHandler = soundListHandler;
- mVibrationListHandler = vibrationListHandler;
- }
-
- @StringRes
- static int getTitleByType(int ringtoneType) {
- switch (ringtoneType) {
- case RingtoneManager.TYPE_ALARM:
- return com.android.internal.R.string.ringtone_picker_title_alarm;
- case RingtoneManager.TYPE_NOTIFICATION:
- return com.android.internal.R.string.ringtone_picker_title_notification;
- default:
- return com.android.internal.R.string.ringtone_picker_title;
- }
- }
-
- static Uri getDefaultItemUriByType(int ringtoneType) {
- switch (ringtoneType) {
- case RingtoneManager.TYPE_ALARM:
- return Settings.System.DEFAULT_ALARM_ALERT_URI;
- case RingtoneManager.TYPE_NOTIFICATION:
- return Settings.System.DEFAULT_NOTIFICATION_URI;
- default:
- return Settings.System.DEFAULT_RINGTONE_URI;
- }
- }
-
- @StringRes
- static int getAddNewItemTextByType(int ringtoneType) {
- switch (ringtoneType) {
- case RingtoneManager.TYPE_ALARM:
- return R.string.add_alarm_text;
- case RingtoneManager.TYPE_NOTIFICATION:
- return R.string.add_notification_text;
- default:
- return R.string.add_ringtone_text;
- }
- }
-
- @StringRes
- static int getDefaultRingtoneItemTextByType(int ringtoneType) {
- switch (ringtoneType) {
- case RingtoneManager.TYPE_ALARM:
- return R.string.alarm_sound_default;
- case RingtoneManager.TYPE_NOTIFICATION:
- return R.string.notification_sound_default;
- default:
- return R.string.ringtone_default;
- }
- }
-
- void init(@NonNull Config pickerConfig,
- RingtoneListHandler.Config soundListConfig,
- RingtoneListHandler.Config vibrationListConfig) {
- mRingtoneManager = mRingtoneManagerFactory.create();
- mPickerConfig = pickerConfig;
- if (mPickerConfig.ringtoneType != RINGTONE_TYPE_UNKNOWN) {
- mRingtoneManager.setType(mPickerConfig.ringtoneType);
- }
- if (soundListConfig != null) {
- mSoundListHandler.init(soundListConfig, mRingtoneManager,
- mRingtoneManager.getCursor());
- }
- if (vibrationListConfig != null) {
- // TODO: Switch to the vibration cursor, once the API is made available.
- mVibrationListHandler.init(vibrationListConfig, mRingtoneManager,
- mRingtoneManager.getCursor());
- }
- }
-
- /**
- * Re-initializes the view model which is required after updating any of the picker lists.
- * This could happen when adding a custom ringtone.
- */
- void reinit() {
- init(mPickerConfig, mSoundListHandler.getRingtoneListConfig(),
- mVibrationListHandler.getRingtoneListConfig());
- }
-
- @NonNull
- Config getPickerConfig() {
- requireInitCalled();
- return mPickerConfig;
- }
-
- @NonNull
- RingtoneListHandler getSoundListHandler() {
- return mSoundListHandler;
- }
-
- @NonNull
- RingtoneListHandler getVibrationListHandler() {
- return mVibrationListHandler;
- }
-
- /**
- * Combined the currently selected sound and vibration URIs and returns a unified URI. If the
- * picker does not show either sound or vibration, that portion of the URI will be null.
- *
- * Currently only the sound URI is returned, since we don't have the API to retrieve vibrations
- * yet.
- * @return Combined sound and vibration URI.
- */
- Uri getSelectedRingtoneUri() {
- // TODO: Combine sound and vibration URIs before returning.
- return mSoundListHandler.getSelectedRingtoneUri();
- }
-
- int getRingtoneStreamType() {
- requireInitCalled();
- return mRingtoneManager.inferStreamType();
- }
-
- void onPause(boolean isChangingConfigurations) {
- if (!isChangingConfigurations) {
- stopAnyPlayingRingtone();
- }
- }
-
- void onStop(boolean isChangingConfigurations) {
- if (isChangingConfigurations) {
- saveAnyPlayingRingtone();
- } else {
- stopAnyPlayingRingtone();
- }
- }
-
- /**
- * Plays a ringtone which is created using the currently selected sound and vibration URIs. If
- * this is a sound or vibration only picker, then the other portion of the URI will be empty
- * and should not affect the played ringtone.
- *
- * Currently, we only use the sound URI to create the ringtone, since we still don't have the
- * API to retrieve the available vibrations list.
- */
- void playRingtone() {
- requireInitCalled();
- stopAnyPlayingRingtone();
-
- mCurrentRingtone = mRingtoneFactory.create(getSelectedRingtoneUri(),
- mPickerConfig.audioAttributesFlags);
-
- if (mCurrentRingtone != null) {
- mCurrentRingtone.play();
- }
- }
-
- /**
- * Cancels all pending async tasks.
- */
- void cancelPendingAsyncTasks() {
- if (mAddCustomRingtoneFuture != null && !mAddCustomRingtoneFuture.isDone()) {
- mAddCustomRingtoneFuture.cancel(/* mayInterruptIfRunning= */ true);
- }
- }
-
- /**
- * Adds an audio file to the list of ringtones asynchronously.
- * Any previous async tasks are canceled before start the new one.
- *
- * @param uri Uri of the file to be added as ringtone. Must be a media file.
- * @param type The type of the ringtone to be added.
- * @param callback The callback to invoke when the task is completed.
- * @param executor The executor to run the callback on when the task completes.
- */
- void addSoundRingtoneAsync(Uri uri, int type, FutureCallback<Uri> callback, Executor executor) {
- // Cancel any currently running add ringtone tasks before starting a new one
- cancelPendingAsyncTasks();
- mAddCustomRingtoneFuture = mListeningExecutorService.submit(
- () -> addRingtone(uri, type));
- Futures.addCallback(mAddCustomRingtoneFuture, callback, executor);
- }
-
- /**
- * Adds an audio file to the list of ringtones.
- *
- * @param uri Uri of the file to be added as ringtone. Must be a media file.
- * @param type The type of the ringtone to be added.
- * @return The Uri of the installed ringtone, which may be the {@code uri} if it
- * is already in ringtone storage. Or null if it failed to add the audio file.
- */
- @Nullable
- private Uri addRingtone(Uri uri, int type) throws IOException {
- requireInitCalled();
- return mRingtoneManager.addCustomExternalRingtone(uri, type);
- }
-
- private void saveAnyPlayingRingtone() {
- if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
- sPlayingRingtone = mCurrentRingtone;
- }
- mCurrentRingtone = null;
- }
-
- private void stopAnyPlayingRingtone() {
- if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) {
- sPlayingRingtone.stop();
- }
- sPlayingRingtone = null;
-
- if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
- mCurrentRingtone.stop();
- }
- mCurrentRingtone = null;
- }
-
- private void requireInitCalled() {
- requireNonNull(mRingtoneManager);
- requireNonNull(mPickerConfig);
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java b/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java
deleted file mode 100644
index 6a34936..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtoneReceiver.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2007 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.soundpicker;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-public class RingtoneReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- if (Intent.ACTION_DEVICE_CUSTOMIZATION_READY.equals(action)) {
- initResourceRingtones(context);
- }
- }
-
- private void initResourceRingtones(Context context) {
- context.startService(
- new Intent(context, RingtoneOverlayService.class));
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java
deleted file mode 100644
index a37191f..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/SoundPickerFragment.java
+++ /dev/null
@@ -1,122 +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.soundpicker;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Environment;
-import android.util.Log;
-import android.view.View;
-import android.widget.Toast;
-
-import androidx.activity.result.ActivityResult;
-import androidx.activity.result.ActivityResultCallback;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.core.content.ContextCompat;
-import androidx.lifecycle.ViewModelProvider;
-
-import com.google.common.util.concurrent.FutureCallback;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A fragment that displays a picker used to select sound or silent. It also includes the
- * ability to add custom sounds.
- */
-public class SoundPickerFragment extends BasePickerFragment {
-
- private static final String TAG = "SoundPickerFragment";
-
- private final FutureCallback<Uri> mAddCustomRingtoneCallback = new FutureCallback<>() {
- @Override
- public void onSuccess(Uri ringtoneUri) {
- requeryForAdapter();
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- Log.e(TAG, "Failed to add custom ringtone.", throwable);
- // Ringtone was not added, display error Toast
- Toast.makeText(requireActivity().getApplicationContext(),
- R.string.unable_to_add_ringtone, Toast.LENGTH_SHORT).show();
- }
- };
-
- ActivityResultLauncher<Intent> mActivityResultLauncher = registerForActivityResult(
- new ActivityResultContracts.StartActivityForResult(),
- new ActivityResultCallback<ActivityResult>() {
- @Override
- public void onActivityResult(ActivityResult result) {
- if (result.getResultCode() == Activity.RESULT_OK) {
- // There are no request codes
- Intent data = result.getData();
- mRingtonePickerViewModel.addSoundRingtoneAsync(data.getData(),
- mPickerConfig.ringtoneType,
- mAddCustomRingtoneCallback,
- // Causes the callback to be executed on the main thread.
- ContextCompat.getMainExecutor(
- requireActivity().getApplicationContext()));
- }
- }
- });
-
- @Override
- public void onViewCreated(@NotNull View view, Bundle savedInstanceState) {
- mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
- RingtonePickerViewModel.class);
- super.onViewCreated(view, savedInstanceState);
- }
-
- @Override
- protected RingtoneListHandler getRingtoneListHandler() {
- return mRingtonePickerViewModel.getSoundListHandler();
- }
-
- @Override
- protected void addRingtoneAsync() {
- // The "Add new ringtone" item was clicked. Start a file picker intent to
- // select only audio files (MIME type "audio/*")
- final Intent chooseFile = getMediaFilePickerIntent();
- mActivityResultLauncher.launch(chooseFile);
- }
-
- @Override
- protected void addNewRingtoneItem() {
- // If external storage is available, add a button to install sounds from storage.
- if (resolvesMediaFilePicker()
- && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- mRingtoneListViewAdapter.addTitleForAddRingtoneItem(
- RingtonePickerViewModel.getAddNewItemTextByType(mPickerConfig.ringtoneType));
- }
- }
-
- private boolean resolvesMediaFilePicker() {
- return getMediaFilePickerIntent().resolveActivity(requireActivity().getPackageManager())
- != null;
- }
-
- private Intent getMediaFilePickerIntent() {
- final Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT);
- chooseFile.setType("audio/*");
- chooseFile.putExtra(Intent.EXTRA_MIME_TYPES,
- new String[]{"audio/*", "application/ogg"});
- return chooseFile;
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java
deleted file mode 100644
index 50ea9d7..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/TabbedDialogFragment.java
+++ /dev/null
@@ -1,180 +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.soundpicker;
-
-import static android.app.Activity.RESULT_CANCELED;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.media.RingtoneManager;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.viewpager2.widget.ViewPager2;
-
-import com.google.android.material.tabs.TabLayout;
-import com.google.android.material.tabs.TabLayoutMediator;
-
-import dagger.hilt.android.AndroidEntryPoint;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A dialog fragment with a sound and/or vibration tab based on the picker type.
- * <ul>
- * <li> Ringtone Pickers will display both sound and vibration tabs.
- * <li> Sound Pickers will only display the sound tab.
- * <li> Vibration Pickers will only display the vibration tab.
- * </ul>
- */
-@AndroidEntryPoint(DialogFragment.class)
-public class TabbedDialogFragment extends Hilt_TabbedDialogFragment {
-
- static final String TAG = "TabbedDialogFragment";
-
- private RingtonePickerViewModel mRingtonePickerViewModel;
-
- private final ViewPager2.OnPageChangeCallback mOnPageChangeCallback =
- new ViewPager2.OnPageChangeCallback() {
- @Override
- public void onPageScrollStateChanged(int state) {
- super.onPageScrollStateChanged(state);
- if (state == ViewPager2.SCROLL_STATE_IDLE) {
- mRingtonePickerViewModel.onStop(/* isChangingConfigurations= */ false);
- }
- }
- };
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
- RingtonePickerViewModel.class);
- }
-
- @NonNull
- @Override
- public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
- AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity(),
- android.R.style.ThemeOverlay_Material_Dialog)
- .setTitle(mRingtonePickerViewModel.getPickerConfig().title);
- // Do not show OK/Cancel buttons in the buttonless (watch-only) version.
- if (mRingtonePickerViewModel.getPickerConfig().showOkCancelButtons) {
- dialogBuilder
- .setPositiveButton(getString(com.android.internal.R.string.ok),
- (dialog, whichButton) -> {
- setSuccessResultWithSelectedRingtone();
- requireActivity().finish();
- })
- .setNegativeButton(getString(com.android.internal.R.string.cancel),
- (dialog, whichButton) -> {
- requireActivity().setResult(RESULT_CANCELED);
- requireActivity().finish();
- });
- }
-
- View view = buildTabbedView(requireActivity().getLayoutInflater());
- dialogBuilder.setView(view);
-
- return dialogBuilder.create();
- }
-
- @Override
- public void onCancel(@NonNull @NotNull DialogInterface dialog) {
- super.onCancel(dialog);
- if (!requireActivity().isChangingConfigurations()) {
- requireActivity().finish();
- }
- }
-
- @Override
- public void onDismiss(@NonNull @NotNull DialogInterface dialog) {
- super.onDismiss(dialog);
- if (!requireActivity().isChangingConfigurations()) {
- requireActivity().finish();
- }
- }
-
- private void setSuccessResultWithSelectedRingtone() {
- requireActivity().setResult(Activity.RESULT_OK,
- new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI,
- mRingtonePickerViewModel.getSelectedRingtoneUri()));
- }
-
- /**
- * Inflates the tabbed layout view and adds the required fragments. If there's only one
- * fragment to display, then the tab area is hidden.
- * @param inflater The LayoutInflater that is used to inflate the tabbed view.
- * @return The tabbed view.
- */
- private View buildTabbedView(@NonNull LayoutInflater inflater) {
- View view = inflater.inflate(R.layout.fragment_tabbed_dialog, null, false);
- TabLayout tabLayout = view.requireViewById(R.id.tabLayout);
- ViewPager2 viewPager = view.requireViewById(R.id.masterViewPager);
-
- ViewPagerAdapter adapter = new ViewPagerAdapter(requireActivity());
- addFragments(adapter);
-
- if (adapter.getItemCount() == 1) {
- // Hide the tab area since there's only one fragment to display.
- tabLayout.setVisibility(View.GONE);
- }
-
- viewPager.setAdapter(adapter);
- viewPager.registerOnPageChangeCallback(mOnPageChangeCallback);
- new TabLayoutMediator(tabLayout, viewPager,
- (tab, position) -> tab.setText(adapter.getTitle(position))).attach();
-
- return view;
- }
-
- /**
- * Adds the appropriate fragments to the adapter based on the PickerType.
- *
- * @param adapter The adapter to add the fragments to.
- */
- private void addFragments(ViewPagerAdapter adapter) {
- switch (mRingtonePickerViewModel.getPickerConfig().mPickerType) {
- case RINGTONE_PICKER:
- adapter.addFragment(getString(R.string.sound_page_title),
- new SoundPickerFragment());
- adapter.addFragment(getString(R.string.vibration_page_title),
- new VibrationPickerFragment());
- break;
- case SOUND_PICKER:
- adapter.addFragment(getString(R.string.sound_page_title),
- new SoundPickerFragment());
- break;
- case VIBRATION_PICKER:
- adapter.addFragment(getString(R.string.vibration_page_title),
- new VibrationPickerFragment());
- break;
- default:
- adapter.addFragment(getString(R.string.sound_page_title),
- new SoundPickerFragment());
- break;
- }
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java b/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java
deleted file mode 100644
index 7412c19..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/VibrationPickerFragment.java
+++ /dev/null
@@ -1,52 +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.soundpicker;
-
-import android.os.Bundle;
-import android.view.View;
-
-import androidx.lifecycle.ViewModelProvider;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A fragment that displays a picker used to select vibration or silent (no vibration).
- */
-public class VibrationPickerFragment extends BasePickerFragment {
-
- @Override
- public void onViewCreated(@NotNull View view, Bundle savedInstanceState) {
- mRingtonePickerViewModel = new ViewModelProvider(requireActivity()).get(
- RingtonePickerViewModel.class);
- super.onViewCreated(view, savedInstanceState);
- }
-
- @Override
- protected RingtoneListHandler getRingtoneListHandler() {
- return mRingtonePickerViewModel.getVibrationListHandler();
- }
-
- @Override
- protected void addRingtoneAsync() {
- // no-op
- }
-
- @Override
- protected void addNewRingtoneItem() {
- // no-op
- }
-}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java b/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java
deleted file mode 100644
index 179068e..0000000
--- a/packages/SoundPicker2/src/com/android/soundpicker/ViewPagerAdapter.java
+++ /dev/null
@@ -1,70 +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.soundpicker;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.viewpager2.adapter.FragmentStateAdapter;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * An adapter used to populate pages inside a ViewPager.
- */
-public class ViewPagerAdapter extends FragmentStateAdapter {
-
- private final List<Fragment> mFragments = new ArrayList<>();
- private final List<String> mTitles = new ArrayList<>();
-
- public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
- super(fragmentActivity);
- }
-
- /**
- * Adds a fragment and page title to the adapter.
- * @param title the title of the page in the ViewPager.
- * @param fragment the fragment that will be inflated on this page.
- */
- public void addFragment(String title, Fragment fragment) {
- mTitles.add(title);
- mFragments.add(fragment);
- }
-
- /**
- * Returns the title of the requested page.
- * @param position the position of the page in the Viewpager.
- * @return The title of the requested page.
- */
- public String getTitle(int position) {
- return mTitles.get(position);
- }
-
- @NonNull
- @Override
- public Fragment createFragment(int position) {
- return Objects.requireNonNull(mFragments.get(position),
- "Could not find a fragment using position: " + position);
- }
-
- @Override
- public int getItemCount() {
- return mFragments.size();
- }
-}
diff --git a/packages/SoundPicker2/tests/Android.bp b/packages/SoundPicker2/tests/Android.bp
deleted file mode 100644
index d88d442..0000000
--- a/packages/SoundPicker2/tests/Android.bp
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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 {
- default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_test {
- name: "SoundPicker2Tests",
- certificate: "platform",
- libs: [
- "android.test.runner",
- "android.test.base",
- ],
- static_libs: [
- "androidx.test.core",
- "androidx.test.rules",
- "androidx.test.ext.junit",
- "androidx.test.ext.truth",
- "mockito-target-minus-junit4",
- "guava-android-testlib",
- "SoundPicker2Lib",
- ],
- srcs: [
- "src/**/*.java",
- ],
-}
diff --git a/packages/SoundPicker2/tests/AndroidManifest.xml b/packages/SoundPicker2/tests/AndroidManifest.xml
deleted file mode 100644
index 295aeb1..0000000
--- a/packages/SoundPicker2/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.soundpicker.tests">
-
- <application android:debuggable="true">
- <uses-library android:name="android.test.runner" />
- </application>
- <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.soundpicker.tests"
- android:label="Sound picker tests">
- </instrumentation>
-</manifest>
diff --git a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java b/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java
deleted file mode 100644
index 80e71e200..0000000
--- a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtoneListHandlerTest.java
+++ /dev/null
@@ -1,167 +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.soundpicker;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.when;
-
-import android.database.Cursor;
-import android.media.RingtoneManager;
-import android.net.Uri;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-public class RingtoneListHandlerTest {
-
- private static final Uri DEFAULT_URI = Uri.parse("media://custom/ringtone/default_uri");
- private static final Uri RINGTONE_URI = Uri.parse("media://custom/ringtone/uri");
- private static final int SILENT_RINGTONE_POSITION = 0;
- private static final int DEFAULT_RINGTONE_POSITION = 1;
- private static final int RINGTONE_POSITION = 2;
-
- @Mock
- private RingtoneManager mMockRingtoneManager;
- @Mock
- private Cursor mMockCursor;
-
- private RingtoneListHandler mRingtoneListHandler;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- RingtoneListHandler.Config mRingtoneListConfig = createRingtoneListConfig();
-
- mRingtoneListHandler = new RingtoneListHandler();
-
- // Add silent and default options to the list.
- mRingtoneListHandler.addSilentItem();
- mRingtoneListHandler.addDefaultItem();
-
- mRingtoneListHandler.init(mRingtoneListConfig, mMockRingtoneManager, mMockCursor);
- }
-
- @Test
- public void testGetRingtoneCursor_returnsTheCorrectRingtoneCursor() {
- assertThat(mRingtoneListHandler.getRingtoneCursor()).isEqualTo(mMockCursor);
- }
-
- @Test
- public void testGetRingtoneUri_returnsTheCorrectRingtoneUri() {
- Uri expectedUri = RINGTONE_URI;
- when(mMockRingtoneManager.getRingtoneUri(eq(0))).thenReturn(expectedUri);
-
- // Request 3rd item from list.
- Uri actualUri = mRingtoneListHandler.getRingtoneUri(RINGTONE_POSITION);
- assertThat(actualUri).isEqualTo(expectedUri);
- }
-
- @Test
- public void testGetRingtoneUri_withSelectedItemUnknown_returnsTheCorrectRingtoneUri() {
- Uri uri = mRingtoneListHandler.getRingtoneUri(RingtoneListHandler.ITEM_POSITION_UNKNOWN);
- assertThat(uri).isEqualTo(RingtoneListHandler.SILENT_URI);
- }
-
- @Test
- public void testGetRingtoneUri_withSelectedItemDefaultPosition_returnsTheCorrectRingtoneUri() {
- Uri actualUri = mRingtoneListHandler.getRingtoneUri(DEFAULT_RINGTONE_POSITION);
- assertThat(actualUri).isEqualTo(DEFAULT_URI);
- }
-
- @Test
- public void testGetRingtoneUri_withSelectedItemSilentPosition_returnsTheCorrectRingtoneUri() {
- Uri uri = mRingtoneListHandler.getRingtoneUri(SILENT_RINGTONE_POSITION);
- assertThat(uri).isEqualTo(RingtoneListHandler.SILENT_URI);
- }
-
- @Test
- public void testGetCurrentlySelectedRingtoneUri_returnsTheCorrectRingtoneUri() {
- mRingtoneListHandler.setSelectedItemPosition(RingtoneListHandler.ITEM_POSITION_UNKNOWN);
- Uri actualUri = mRingtoneListHandler.getSelectedRingtoneUri();
- assertThat(actualUri).isEqualTo(RingtoneListHandler.SILENT_URI);
-
- mRingtoneListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION);
- actualUri = mRingtoneListHandler.getSelectedRingtoneUri();
- assertThat(actualUri).isEqualTo(DEFAULT_URI);
-
- mRingtoneListHandler.setSelectedItemPosition(SILENT_RINGTONE_POSITION);
- actualUri = mRingtoneListHandler.getSelectedRingtoneUri();
- assertThat(actualUri).isEqualTo(RingtoneListHandler.SILENT_URI);
-
- when(mMockRingtoneManager.getRingtoneUri(eq(0))).thenReturn(RINGTONE_URI);
- mRingtoneListHandler.setSelectedItemPosition(RINGTONE_POSITION);
- actualUri = mRingtoneListHandler.getSelectedRingtoneUri();
- assertThat(actualUri).isEqualTo(RINGTONE_URI);
- }
-
- @Test
- public void testGetRingtonePosition_returnsTheCorrectRingtonePosition() {
- when(mMockRingtoneManager.getRingtonePosition(RINGTONE_URI)).thenReturn(0);
-
- int actualPosition = mRingtoneListHandler.getRingtonePosition(RINGTONE_URI);
-
- assertThat(actualPosition).isEqualTo(RINGTONE_POSITION);
-
- }
-
- @Test
- public void testFixedItems_onlyAddsItemsOnceAndInOrder() {
- // Clear fixed items before testing the add methods.
- mRingtoneListHandler.resetFixedItems();
-
- assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo(
- RingtoneListHandler.ITEM_POSITION_UNKNOWN);
- assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo(
- RingtoneListHandler.ITEM_POSITION_UNKNOWN);
-
- mRingtoneListHandler.addSilentItem();
- mRingtoneListHandler.addDefaultItem();
- mRingtoneListHandler.addSilentItem();
- mRingtoneListHandler.addDefaultItem();
-
- assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo(
- SILENT_RINGTONE_POSITION);
- assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo(
- DEFAULT_RINGTONE_POSITION);
- }
-
- @Test
- public void testResetFixedItems_resetsSilentAndDefaultItemPositions() {
- assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo(
- SILENT_RINGTONE_POSITION);
- assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo(
- DEFAULT_RINGTONE_POSITION);
-
- mRingtoneListHandler.resetFixedItems();
-
- assertThat(mRingtoneListHandler.getSilentItemPosition()).isEqualTo(
- RingtoneListHandler.ITEM_POSITION_UNKNOWN);
- assertThat(mRingtoneListHandler.getDefaultItemPosition()).isEqualTo(
- RingtoneListHandler.ITEM_POSITION_UNKNOWN);
- }
-
- private RingtoneListHandler.Config createRingtoneListConfig() {
- return new RingtoneListHandler.Config(/* hasDefaultItem= */ true,
- /* uriForDefaultItem= */ DEFAULT_URI, /* hasSilentItem= */ true,
- /* existingUri= */ DEFAULT_URI);
- }
-}
diff --git a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java b/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java
deleted file mode 100644
index cde6c76..0000000
--- a/packages/SoundPicker2/tests/src/com/android/soundpicker/RingtonePickerViewModelTest.java
+++ /dev/null
@@ -1,534 +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.soundpicker;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.database.Cursor;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.provider.Settings;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.testing.TestingExecutors;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.IOException;
-import java.util.concurrent.ExecutorService;
-
-@RunWith(AndroidJUnit4.class)
-public class RingtonePickerViewModelTest {
-
- private static final Uri DEFAULT_URI = Uri.parse("media://custom/ringtone/default_uri");
- private static final Uri RINGTONE_URI = Uri.parse("media://custom/ringtone/uri");
- private static final int RINGTONE_TYPE_UNKNOWN = -1;
- private static final int DEFAULT_RINGTONE_POSITION = 1;
-
- @Mock
- private RingtoneManagerFactory mMockRingtoneManagerFactory;
- @Mock
- private RingtoneFactory mMockRingtoneFactory;
- @Mock
- private RingtoneManager mMockRingtoneManager;
- @Mock
- private ListeningExecutorServiceFactory mMockListeningExecutorServiceFactory;
- @Mock
- private Cursor mMockCursor;
-
- private RingtoneListHandler mSoundListHandler;
- private RingtoneListHandler mVibrationListHandler;
- private ExecutorService mMainThreadExecutor;
- private ListeningExecutorService mBackgroundThreadExecutor;
- private Ringtone mMockDefaultRingtone;
- private Ringtone mMockRingtone;
- private RingtonePickerViewModel mViewModel;
- private RingtoneListHandler.Config mSoundListConfig;
- private RingtoneListHandler.Config mVibrationListConfig;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
-
- mSoundListHandler = new RingtoneListHandler();
- mVibrationListHandler = new RingtoneListHandler();
- mSoundListConfig = createRingtoneListConfig();
- mVibrationListConfig = createRingtoneListConfig();
- mMockDefaultRingtone = createMockRingtone();
- mMockRingtone = createMockRingtone();
- when(mMockRingtoneManagerFactory.create()).thenReturn(mMockRingtoneManager);
- when(mMockRingtoneFactory.create(DEFAULT_URI,
- AudioAttributes.FLAG_AUDIBILITY_ENFORCED)).thenReturn(mMockDefaultRingtone);
- when(mMockRingtoneManager.getRingtoneUri(anyInt())).thenReturn(RINGTONE_URI);
- when(mMockRingtoneManager.getCursor()).thenReturn(mMockCursor);
- mMainThreadExecutor = TestingExecutors.sameThreadScheduledExecutor();
- mBackgroundThreadExecutor = TestingExecutors.sameThreadScheduledExecutor();
- when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn(
- mBackgroundThreadExecutor);
-
- mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
- mMockListeningExecutorServiceFactory, mSoundListHandler,
- mVibrationListHandler);
-
- // Add silent and default options to the sound list.
- mSoundListHandler.addSilentItem();
- mSoundListHandler.addDefaultItem();
-
- // Add silent and default options to the vibration list.
- mVibrationListHandler.addSilentItem();
- mVibrationListHandler.addDefaultItem();
-
- mSoundListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION);
- mVibrationListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION);
- }
-
- @After
- public void teardown() {
- if (mMainThreadExecutor != null && !mMainThreadExecutor.isShutdown()) {
- mMainThreadExecutor.shutdown();
- }
- if (mBackgroundThreadExecutor != null && !mBackgroundThreadExecutor.isShutdown()) {
- mBackgroundThreadExecutor.shutdown();
- }
- }
-
- @Test
- public void testInitRingtoneManager_whenTypeIsUnknown_createManagerButDoNotSetType() {
- mViewModel.init(createPickerConfig(RINGTONE_TYPE_UNKNOWN), mSoundListConfig,
- mVibrationListConfig);
-
- verify(mMockRingtoneManagerFactory).create();
- verify(mMockRingtoneManager, never()).setType(anyInt());
- assertNotNull(mViewModel.getSoundListHandler().getRingtoneListConfig());
- assertNotNull(mViewModel.getVibrationListHandler().getRingtoneListConfig());
- }
-
- @Test
- public void testInitRingtoneManager_whenTypeIsNotUnknown_createManagerAndSetType() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_NOTIFICATION), mSoundListConfig,
- mVibrationListConfig);
-
- verify(mMockRingtoneManagerFactory).create();
- verify(mMockRingtoneManager).setType(RingtoneManager.TYPE_NOTIFICATION);
- assertNotNull(mViewModel.getSoundListHandler().getRingtoneListConfig());
- assertNotNull(mViewModel.getVibrationListHandler().getRingtoneListConfig());
- }
-
- @Test
- public void testInitRingtoneManager_bothListConfigsAreNull_onlyRecreateRingtoneManager() {
- mViewModel.init(
- createPickerConfig(RingtoneManager.TYPE_NOTIFICATION),
- /* soundListConfig= */ null, /* vibrationListConfig= */ null);
-
- verify(mMockRingtoneManagerFactory).create();
- verify(mMockRingtoneManager).setType(RingtoneManager.TYPE_NOTIFICATION);
- assertNull(mViewModel.getSoundListHandler().getRingtoneListConfig());
- assertNull(mViewModel.getVibrationListHandler().getRingtoneListConfig());
- }
-
- @Test
- public void testReinitialize_bothListConfigsInitialized_recreateManagerAndReinitHandlers() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_NOTIFICATION), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.reinit();
-
- verify(mMockRingtoneManagerFactory, times(2)).create();
- verify(mMockRingtoneManager, times(2)).setType(RingtoneManager.TYPE_NOTIFICATION);
- assertNotNull(mViewModel.getSoundListHandler().getRingtoneListConfig());
- assertNotNull(mViewModel.getVibrationListHandler().getRingtoneListConfig());
- }
-
- @Test
- public void testReinitialize_bothListConfigsAlreadyNull_onlyRecreateRingtoneManager() {
- mViewModel.init(
- createPickerConfig(RingtoneManager.TYPE_NOTIFICATION),
- /* soundListConfig= */ null, /* vibrationListConfig= */ null);
- mViewModel.reinit();
-
- verify(mMockRingtoneManagerFactory, times(2)).create();
- verify(mMockRingtoneManager, times(2)).setType(RingtoneManager.TYPE_NOTIFICATION);
- assertNull(mViewModel.getSoundListHandler().getRingtoneListConfig());
- assertNull(mViewModel.getVibrationListHandler().getRingtoneListConfig());
- }
-
- @Test
- public void testGetStreamType_returnsTheCorrectStreamType() {
- when(mMockRingtoneManager.inferStreamType()).thenReturn(AudioManager.STREAM_ALARM);
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- assertEquals(mViewModel.getRingtoneStreamType(), AudioManager.STREAM_ALARM);
- }
-
- @Test
- public void testOnPause_withChangingConfigurationTrue_doNotStopPlayingRingtone() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- mViewModel.onPause(/* isChangingConfigurations= */ true);
- verify(mMockDefaultRingtone, never()).stop();
- }
-
- @Test
- public void testOnPause_withChangingConfigurationFalse_stopPlayingRingtone() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- mViewModel.onPause(/* isChangingConfigurations= */ false);
- verify(mMockDefaultRingtone).stop();
- }
-
- @Test
- public void testOnViewModelRecreated_previousRingtoneCanStillBeStopped() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- Ringtone mockRingtone1 = createMockRingtone();
- Ringtone mockRingtone2 = createMockRingtone();
-
- when(mMockRingtoneFactory.create(any(), anyInt())).thenReturn(mockRingtone1, mockRingtone2);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mockRingtone1);
- // Fake a scenario where the activity is destroyed and recreated due to a config change.
- // This will result in a new view model getting created.
- mViewModel.onStop(/* isChangingConfigurations= */ true);
- verify(mockRingtone1, never()).stop();
- mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
- mMockListeningExecutorServiceFactory, mSoundListHandler,
- mVibrationListHandler);
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mockRingtone2);
- verify(mockRingtone1).stop();
- verify(mockRingtone2, never()).stop();
- }
-
- @Test
- public void testOnStop_withChangingConfigurationTrueAndDefaultRingtonePlaying_saveRingtone() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- mViewModel.onStop(/* isChangingConfigurations= */ true);
- assertEquals(RingtonePickerViewModel.sPlayingRingtone, mMockDefaultRingtone);
- }
-
- @Test
- public void testOnStop_withChangingConfigurationTrueAndCurrentRingtonePlaying_saveRingtone() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- mViewModel.onStop(/* isChangingConfigurations= */ true);
- assertEquals(RingtonePickerViewModel.sPlayingRingtone, mMockDefaultRingtone);
- }
-
- @Test
- public void testOnStop_withChangingConfigurationTrueAndNoPlayingRingtone_saveNothing() {
- mViewModel.onStop(/* isChangingConfigurations= */ true);
- assertNull(RingtonePickerViewModel.sPlayingRingtone);
- }
-
- @Test
- public void testOnStop_withChangingConfigurationFalse_stopPlayingRingtone() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
-
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- mViewModel.onStop(/* isChangingConfigurations= */ false);
- verify(mMockDefaultRingtone).stop();
- }
-
- @Test
- public void testGetCurrentlySelectedRingtoneUri_returnsTheCorrectRingtoneUri() {
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
-
- assertEquals(DEFAULT_URI, mViewModel.getSelectedRingtoneUri());
- }
-
- @Test
- public void testPlayRingtone_playTheCorrectRingtone() {
- mSoundListHandler.setSelectedItemPosition(DEFAULT_RINGTONE_POSITION);
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
-
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- }
-
- @Test
- public void testPlayRingtone_stopsPreviouslyRunningRingtone() {
- // Start playing the first ringtone
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockDefaultRingtone);
- // Start playing the second ringtone
- when(mMockRingtoneFactory.create(DEFAULT_URI,
- AudioAttributes.FLAG_AUDIBILITY_ENFORCED)).thenReturn(mMockRingtone);
- mViewModel.playRingtone();
- verifyRingtonePlayCalledAndMockPlayingState(mMockRingtone);
-
- verify(mMockDefaultRingtone).stop();
- }
-
- @Test
- public void testDefaultItemUri_withNotificationIntent_returnDefaultNotificationUri() {
- Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(
- RingtoneManager.TYPE_NOTIFICATION);
- assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, uri);
- }
-
- @Test
- public void testDefaultItemUri_withAlarmIntent_returnDefaultAlarmUri() {
- Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(RingtoneManager.TYPE_ALARM);
- assertEquals(Settings.System.DEFAULT_ALARM_ALERT_URI, uri);
- }
-
- @Test
- public void testDefaultItemUri_withRingtoneIntent_returnDefaultRingtoneUri() {
- Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(RingtoneManager.TYPE_RINGTONE);
- assertEquals(Settings.System.DEFAULT_RINGTONE_URI, uri);
- }
-
- @Test
- public void testDefaultItemUri_withInvalidRingtoneType_returnDefaultRingtoneUri() {
- Uri uri = RingtonePickerViewModel.getDefaultItemUriByType(-1);
- assertEquals(Settings.System.DEFAULT_RINGTONE_URI, uri);
- }
-
- @Test
- public void testTitle_withNotificationRingtoneType_returnRingtoneNotificationTitle() {
- int title = RingtonePickerViewModel.getTitleByType(RingtoneManager.TYPE_NOTIFICATION);
- assertEquals(com.android.internal.R.string.ringtone_picker_title_notification, title);
- }
-
- @Test
- public void testTitle_withAlarmRingtoneType_returnRingtoneAlarmTitle() {
- int title = RingtonePickerViewModel.getTitleByType(RingtoneManager.TYPE_ALARM);
- assertEquals(com.android.internal.R.string.ringtone_picker_title_alarm, title);
- }
-
- @Test
- public void testTitle_withInvalidRingtoneType_returnDefaultRingtoneTitle() {
- int title = RingtonePickerViewModel.getTitleByType(/*ringtoneType= */ -1);
- assertEquals(com.android.internal.R.string.ringtone_picker_title, title);
- }
-
- @Test
- public void testAddNewItemText_withAlarmType_returnAlarmAddItemText() {
- int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType(
- RingtoneManager.TYPE_ALARM);
- assertEquals(R.string.add_alarm_text, addNewItemTextResId);
- }
-
- @Test
- public void testAddNewItemText_withNotificationType_returnNotificationAddItemText() {
- int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType(
- RingtoneManager.TYPE_NOTIFICATION);
- assertEquals(R.string.add_notification_text, addNewItemTextResId);
- }
-
- @Test
- public void testAddNewItemText_withRingtoneType_returnRingtoneAddItemText() {
- int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType(
- RingtoneManager.TYPE_RINGTONE);
- assertEquals(R.string.add_ringtone_text, addNewItemTextResId);
- }
-
- @Test
- public void testAddNewItemText_withInvalidType_returnRingtoneAddItemText() {
- int addNewItemTextResId = RingtonePickerViewModel.getAddNewItemTextByType(-1);
- assertEquals(R.string.add_ringtone_text, addNewItemTextResId);
- }
-
- @Test
- public void testDefaultItemText_withNotificationType_returnNotificationDefaultItemText() {
- int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType(
- RingtoneManager.TYPE_NOTIFICATION);
- assertEquals(R.string.notification_sound_default, defaultRingtoneItemText);
- }
-
- @Test
- public void testDefaultItemText_withAlarmType_returnAlarmDefaultItemText() {
- int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType(
- RingtoneManager.TYPE_NOTIFICATION);
- assertEquals(R.string.notification_sound_default, defaultRingtoneItemText);
- }
-
- @Test
- public void testDefaultItemText_withRingtoneType_returnRingtoneDefaultItemText() {
- int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType(
- RingtoneManager.TYPE_RINGTONE);
- assertEquals(R.string.ringtone_default, defaultRingtoneItemText);
- }
-
- @Test
- public void testDefaultItemText_withInvalidType_returnRingtoneDefaultItemText() {
- int defaultRingtoneItemText = RingtonePickerViewModel.getDefaultRingtoneItemTextByType(-1);
- assertEquals(R.string.ringtone_default, defaultRingtoneItemText);
- }
-
- @Test
- public void testCancelPendingAsyncTasks_correctlyCancelsPendingTasks()
- throws IOException {
- FutureCallback<Uri> mockCallback = mock(FutureCallback.class);
-
- when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn(
- TestingExecutors.noOpScheduledExecutor());
-
- mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
- mMockListeningExecutorServiceFactory, mSoundListHandler,
- mVibrationListHandler);
- mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION,
- mockCallback, mMainThreadExecutor);
- verify(mockCallback, never()).onFailure(any());
- // Calling cancelPendingAsyncTasks should cancel the pending task. Cancelling an async
- // task invokes the onFailure method in the callable.
- mViewModel.cancelPendingAsyncTasks();
- verify(mockCallback).onFailure(any());
- verify(mockCallback, never()).onSuccess(any());
-
- }
-
- @Test
- public void testAddRingtoneAsync_cancelPreviousTaskBeforeStartingNewOne()
- throws IOException {
- FutureCallback<Uri> mockCallback1 = mock(FutureCallback.class);
- FutureCallback<Uri> mockCallback2 = mock(FutureCallback.class);
-
- when(mMockListeningExecutorServiceFactory.createSingleThreadExecutor()).thenReturn(
- TestingExecutors.noOpScheduledExecutor());
-
- mViewModel = new RingtonePickerViewModel(mMockRingtoneManagerFactory, mMockRingtoneFactory,
- mMockListeningExecutorServiceFactory, mSoundListHandler,
- mVibrationListHandler);
- mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION,
- mockCallback1, mMainThreadExecutor);
- verify(mockCallback1, never()).onFailure(any());
- // We call addRingtoneAsync again to cancel the previous task and start a new one.
- // Cancelling an async task invokes the onFailure method in the callable.
- mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION,
- mockCallback2, mMainThreadExecutor);
- verify(mockCallback1).onFailure(any());
- verify(mockCallback1, never()).onSuccess(any());
- verifyNoMoreInteractions(mockCallback2);
- }
-
- @Test
- public void testAddRingtoneAsync_whenAddRingtoneIsSuccessful_successCallbackIsInvoked()
- throws IOException {
- Uri expectedUri = DEFAULT_URI;
- FutureCallback<Uri> mockCallback = mock(FutureCallback.class);
-
- when(mMockRingtoneManager.addCustomExternalRingtone(DEFAULT_URI,
- RingtoneManager.TYPE_NOTIFICATION)).thenReturn(expectedUri);
-
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
-
- mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION,
- mockCallback, mMainThreadExecutor);
-
- verify(mockCallback).onSuccess(expectedUri);
- verify(mockCallback, never()).onFailure(any());
- }
-
- @Test
- public void testAddRingtoneAsync_whenAddRingtoneFailed_failureCallbackIsInvoked()
- throws IOException {
- FutureCallback<Uri> mockCallback = mock(FutureCallback.class);
-
- when(mMockRingtoneManager.addCustomExternalRingtone(any(), anyInt())).thenThrow(
- IOException.class);
-
- mViewModel.init(createPickerConfig(RingtoneManager.TYPE_RINGTONE), mSoundListConfig,
- mVibrationListConfig);
-
- mViewModel.addSoundRingtoneAsync(DEFAULT_URI, RingtoneManager.TYPE_NOTIFICATION,
- mockCallback, mMainThreadExecutor);
-
- verify(mockCallback).onFailure(any(IOException.class));
- verify(mockCallback, never()).onSuccess(any());
- }
-
- private Ringtone createMockRingtone() {
- Ringtone mockRingtone = mock(Ringtone.class);
- when(mockRingtone.getAudioAttributes()).thenReturn(
- audioAttributes(AudioAttributes.USAGE_NOTIFICATION_RINGTONE, 0));
-
- return mockRingtone;
- }
-
- private void verifyRingtonePlayCalledAndMockPlayingState(Ringtone ringtone) {
- verify(ringtone).play();
- when(ringtone.isPlaying()).thenReturn(true);
- }
-
- private static AudioAttributes audioAttributes(int audioUsage, int flags) {
- return new AudioAttributes.Builder()
- .setUsage(audioUsage)
- .setFlags(flags)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build();
- }
-
- private RingtonePickerViewModel.Config createPickerConfig(int ringtoneType,
- int audioAttributes) {
- return new RingtonePickerViewModel.Config("Phone ringtone", /* userId= */ 1,
- ringtoneType, /* showOkCancelButtons= */ true,
- audioAttributes, RingtonePickerViewModel.PickerType.RINGTONE_PICKER);
- }
-
- private RingtonePickerViewModel.Config createPickerConfig(int ringtoneType) {
- return new RingtonePickerViewModel.Config("Phone ringtone", /* userId= */ 1,
- ringtoneType, /* showOkCancelButtons= */ true,
- AudioAttributes.FLAG_AUDIBILITY_ENFORCED,
- RingtonePickerViewModel.PickerType.RINGTONE_PICKER);
- }
-
- private RingtoneListHandler.Config createRingtoneListConfig() {
- return new RingtoneListHandler.Config(/* hasDefaultItem= */ true,
- /* uriForDefaultItem= */ DEFAULT_URI, /* hasSilentItem= */ true,
- /* existingUri= */ Uri.parse(""));
- }
-}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index 8194055..4973caf 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -53,12 +53,12 @@
*/
class ActivityLaunchAnimator(
/** The animator used when animating a View into an app. */
- private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR,
+ private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
/** The animator used when animating a Dialog into an app. */
// TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to
// TIMINGS.contentBeforeFadeOutDuration.
- private val dialogToAppAnimator: LaunchAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR,
+ private val dialogToAppAnimator: TransitionAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR,
/**
* Whether we should disable the WindowManager timeout. This should be set to true in tests
@@ -71,7 +71,7 @@
/** The timings when animating a View into an app. */
@JvmField
val TIMINGS =
- LaunchAnimator.Timings(
+ TransitionAnimator.Timings(
totalDuration = 500L,
contentBeforeFadeOutDelay = 0L,
contentBeforeFadeOutDuration = 150L,
@@ -89,7 +89,7 @@
/** The interpolators when animating a View or a dialog into an app. */
val INTERPOLATORS =
- LaunchAnimator.Interpolators(
+ TransitionAnimator.Interpolators(
positionInterpolator = Interpolators.EMPHASIZED,
positionXInterpolator = Interpolators.EMPHASIZED_COMPLEMENT,
contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
@@ -99,8 +99,9 @@
// TODO(b/288507023): Remove this flag.
@JvmField val DEBUG_LAUNCH_ANIMATION = Build.IS_DEBUGGABLE
- private val DEFAULT_LAUNCH_ANIMATOR = LaunchAnimator(TIMINGS, INTERPOLATORS)
- private val DEFAULT_DIALOG_TO_APP_ANIMATOR = LaunchAnimator(DIALOG_TIMINGS, INTERPOLATORS)
+ private val DEFAULT_TRANSITION_ANIMATOR = TransitionAnimator(TIMINGS, INTERPOLATORS)
+ private val DEFAULT_DIALOG_TO_APP_ANIMATOR =
+ TransitionAnimator(DIALOG_TIMINGS, INTERPOLATORS)
/** Durations & interpolators for the navigation bar fading in & out. */
private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
@@ -154,7 +155,7 @@
* Start an intent and animate the opening window. The intent will be started by running
* [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch
* result. [controller] is responsible from animating the view from which the intent was started
- * in [Controller.onLaunchAnimationProgress]. No animation will start if there is no window
+ * in [Controller.onTransitionAnimationProgress]. No animation will start if there is no window
* opening.
*
* If [controller] is null or [animate] is false, then the intent will be started and no
@@ -255,7 +256,7 @@
private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) {
if (Looper.myLooper() != Looper.getMainLooper()) {
- this.launchContainer.context.mainExecutor.execute {
+ this.transitionContainer.context.mainExecutor.execute {
callOnIntentStartedOnMainThread(willAnimate)
}
} else {
@@ -306,14 +307,14 @@
@VisibleForTesting
fun createRunner(controller: Controller): Runner {
// Make sure we use the modified timings when animating a dialog into an app.
- val launchAnimator =
+ val transitionAnimator =
if (controller.isDialogLaunch) {
dialogToAppAnimator
} else {
- launchAnimator
+ transitionAnimator
}
- return Runner(controller, callback!!, launchAnimator, lifecycleListener)
+ return Runner(controller, callback!!, transitionAnimator, lifecycleListener)
}
interface PendingIntentStarter {
@@ -364,7 +365,7 @@
*
* Note that all callbacks (onXXX methods) are all called on the main thread.
*/
- interface Controller : LaunchAnimator.Controller {
+ interface Controller : TransitionAnimator.Controller {
companion object {
/**
* Return a [Controller] that will animate and expand [view] into the opening window.
@@ -427,9 +428,9 @@
fun onIntentStarted(willAnimate: Boolean) {}
/**
- * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after
- * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
- * before the cancellation.
+ * The animation was cancelled. Note that [onTransitionAnimationEnd] will still be called
+ * after this if the animation was already started, i.e. if [onTransitionAnimationStart] was
+ * called before the cancellation.
*
* If this launch animation affected the occlusion state of the keyguard, WM will provide us
* with [newKeyguardOccludedState] so that we can set the occluded state appropriately.
@@ -475,11 +476,11 @@
controller: Controller,
callback: Callback,
/** The animator to use to animate the window launch. */
- launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR,
+ transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
/** Listener for animation lifecycle events. */
listener: Listener? = null
) : IRemoteAnimationRunner.Stub() {
- private val context = controller.launchContainer.context
+ private val context = controller.transitionContainer.context
// This is being passed across IPC boundaries and cycles (through PendingIntentRecords,
// etc.) are possible. So we need to make sure we drop any references that might
@@ -492,7 +493,7 @@
controller,
callback,
DelegatingAnimationCompletionListener(listener, this::dispose),
- launchAnimator,
+ transitionAnimator,
disableWmTimeout
)
}
@@ -543,7 +544,7 @@
/** Listener for animation lifecycle events. */
private val listener: Listener? = null,
/** The animator to use to animate the window launch. */
- private val launchAnimator: LaunchAnimator = DEFAULT_LAUNCH_ANIMATOR,
+ private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
/**
* Whether we should disable the WindowManager timeout. This should be set to true in tests
@@ -552,10 +553,10 @@
// TODO(b/301385865): Remove this flag.
disableWmTimeout: Boolean = false,
) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> {
- private val launchContainer = controller.launchContainer
- private val context = launchContainer.context
+ private val transitionContainer = controller.transitionContainer
+ private val context = transitionContainer.context
private val transactionApplierView =
- controller.openingWindowSyncView ?: controller.launchContainer
+ controller.openingWindowSyncView ?: controller.transitionContainer
private val transactionApplier = SyncRtSurfaceTransactionApplier(transactionApplierView)
private val timeoutHandler =
if (!disableWmTimeout) {
@@ -570,7 +571,7 @@
private var windowCropF = RectF()
private var timedOut = false
private var cancelled = false
- private var animation: LaunchAnimator.Animation? = null
+ private var animation: TransitionAnimator.Animation? = null
/**
* A timeout to cancel the launch animation if the remote animation is not started or
@@ -660,7 +661,7 @@
nonApps: Array<out RemoteAnimationTarget>?,
iCallback: IRemoteAnimationFinishedCallback?
) {
- if (LaunchAnimator.DEBUG) {
+ if (TransitionAnimator.DEBUG) {
Log.d(TAG, "Remote animation started")
}
@@ -687,7 +688,7 @@
val windowBounds = window.screenSpaceBounds
val endState =
- LaunchAnimator.State(
+ TransitionAnimator.State(
top = windowBounds.top,
bottom = windowBounds.bottom,
left = windowBounds.left,
@@ -699,7 +700,7 @@
// TODO(b/184121838): We should somehow get the top and bottom radius of the window
// instead of recomputing isExpandingFullyAbove here.
val isExpandingFullyAbove =
- launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState)
+ transitionAnimator.isExpandingFullyAbove(controller.transitionContainer, endState)
val endRadius =
if (isExpandingFullyAbove) {
// Most of the time, expanding fully above the root view means expanding in full
@@ -718,7 +719,7 @@
val delegate = this.controller
val controller =
object : Controller by delegate {
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
listener?.onLaunchAnimationStart()
if (DEBUG_LAUNCH_ANIMATION) {
@@ -728,10 +729,10 @@
"$isExpandingFullyAbove) [controller=$delegate]"
)
}
- delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
listener?.onLaunchAnimationEnd()
iCallback?.invoke()
@@ -742,11 +743,11 @@
"$isExpandingFullyAbove) [controller=$delegate]"
)
}
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
@@ -758,12 +759,12 @@
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
listener?.onLaunchAnimationProgress(linearProgress)
- delegate.onLaunchAnimationProgress(state, progress, linearProgress)
+ delegate.onTransitionAnimationProgress(state, progress, linearProgress)
}
}
animation =
- launchAnimator.startAnimation(
+ transitionAnimator.startAnimation(
controller,
endState,
windowBackgroundColor,
@@ -774,7 +775,7 @@
private fun applyStateToWindow(
window: RemoteAnimationTarget,
- state: LaunchAnimator.State,
+ state: TransitionAnimator.State,
linearProgress: Float,
) {
if (transactionApplierView.viewRootImpl == null || !window.leash.isValid) {
@@ -825,7 +826,7 @@
val alpha =
if (controller.isBelowAnimatingWindow) {
val windowProgress =
- LaunchAnimator.getProgress(
+ TransitionAnimator.getProgress(
TIMINGS,
linearProgress,
TIMINGS.contentAfterFadeInDelay,
@@ -857,7 +858,7 @@
private fun applyStateToNavigationBar(
navigationBar: RemoteAnimationTarget,
- state: LaunchAnimator.State,
+ state: TransitionAnimator.State,
linearProgress: Float
) {
if (transactionApplierView.viewRootImpl == null || !navigationBar.leash.isValid) {
@@ -868,7 +869,7 @@
}
val fadeInProgress =
- LaunchAnimator.getProgress(
+ TransitionAnimator.getProgress(
TIMINGS,
linearProgress,
ANIMATION_DELAY_NAV_FADE_IN,
@@ -890,7 +891,7 @@
.withVisibility(true)
} else {
val fadeOutProgress =
- LaunchAnimator.getProgress(
+ TransitionAnimator.getProgress(
TIMINGS,
linearProgress,
0,
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index 168039e..9a36960 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -58,7 +58,7 @@
private val callback: Callback,
private val interactionJankMonitor: InteractionJankMonitor,
private val featureFlags: AnimationFeatureFlags,
- private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
+ private val transitionAnimator: TransitionAnimator = TransitionAnimator(TIMINGS, INTERPOLATORS),
private val isForTesting: Boolean = false,
) {
private companion object {
@@ -108,21 +108,21 @@
fun stopDrawingInOverlay()
/**
- * Create the [LaunchAnimator.Controller] that will be called to animate the source
+ * Create the [TransitionAnimator.Controller] that will be called to animate the source
* controlled by this [Controller] during the dialog launch animation.
*
* At the end of this animation, the source should *not* be visible anymore (until the
* dialog is closed and is animated back into the source).
*/
- fun createLaunchController(): LaunchAnimator.Controller
+ fun createTransitionController(): TransitionAnimator.Controller
/**
- * Create the [LaunchAnimator.Controller] that will be called to animate the source
+ * Create the [TransitionAnimator.Controller] that will be called to animate the source
* controlled by this [Controller] during the dialog exit animation.
*
* At the end of this animation, the source should be visible again.
*/
- fun createExitController(): LaunchAnimator.Controller
+ fun createExitController(): TransitionAnimator.Controller
/**
* Whether we should animate the dialog back into the source when it is dismissed. If this
@@ -270,7 +270,7 @@
val animatedDialog =
AnimatedDialog(
- launchAnimator = launchAnimator,
+ transitionAnimator = transitionAnimator,
callback = callback,
interactionJankMonitor = interactionJankMonitor,
controller = controller,
@@ -406,8 +406,8 @@
dialog.dismiss()
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
- controller.onLaunchAnimationStart(isExpandingFullyAbove)
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ controller.onTransitionAnimationStart(isExpandingFullyAbove)
// Make sure the dialog is not dismissed during the animation.
disableDialogDismiss()
@@ -420,8 +420,8 @@
dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- controller.onLaunchAnimationEnd(isExpandingFullyAbove)
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove)
// Hide the dialog then dismiss it to instantly dismiss it without playing the
// animation.
@@ -492,7 +492,7 @@
data class DialogCuj(@CujType val cujType: Int, val tag: String? = null)
private class AnimatedDialog(
- private val launchAnimator: LaunchAnimator,
+ private val transitionAnimator: TransitionAnimator,
private val callback: DialogLaunchAnimator.Callback,
private val interactionJankMonitor: InteractionJankMonitor,
@@ -892,7 +892,7 @@
// Create 2 controllers to animate both the dialog and the source.
val startController =
if (isLaunching) {
- controller.createLaunchController()
+ controller.createTransitionController()
} else {
GhostedViewLaunchAnimatorController(dialogContentWithBackground!!)
}
@@ -902,34 +902,34 @@
} else {
controller.createExitController()
}
- startController.launchContainer = decorView
- endController.launchContainer = decorView
+ startController.transitionContainer = decorView
+ endController.transitionContainer = decorView
val endState = endController.createAnimatorState()
val controller =
- object : LaunchAnimator.Controller {
- override var launchContainer: ViewGroup
- get() = startController.launchContainer
+ object : TransitionAnimator.Controller {
+ override var transitionContainer: ViewGroup
+ get() = startController.transitionContainer
set(value) {
- startController.launchContainer = value
- endController.launchContainer = value
+ startController.transitionContainer = value
+ endController.transitionContainer = value
}
- override fun createAnimatorState(): LaunchAnimator.State {
+ override fun createAnimatorState(): TransitionAnimator.State {
return startController.createAnimatorState()
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
// During launch, onLaunchAnimationStart will be used to remove the temporary
// touch surface ghost so it is important to call this before calling
// onLaunchAnimationStart on the controller (which will create its own ghost).
onLaunchAnimationStart()
- startController.onLaunchAnimationStart(isExpandingFullyAbove)
- endController.onLaunchAnimationStart(isExpandingFullyAbove)
+ startController.onTransitionAnimationStart(isExpandingFullyAbove)
+ endController.onTransitionAnimationStart(isExpandingFullyAbove)
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
// onLaunchAnimationEnd is called by an Animator at the end of the animation,
// on a Choreographer animation tick. The following calls will move the animated
// content from the dialog overlay back to its original position, and this
@@ -943,23 +943,23 @@
// that the move of the content back to its original window will be reflected in
// the next frame right after [onLaunchAnimationEnd] is called.
dialog.context.mainExecutor.execute {
- startController.onLaunchAnimationEnd(isExpandingFullyAbove)
- endController.onLaunchAnimationEnd(isExpandingFullyAbove)
+ startController.onTransitionAnimationEnd(isExpandingFullyAbove)
+ endController.onTransitionAnimationEnd(isExpandingFullyAbove)
onLaunchAnimationEnd()
}
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
- startController.onLaunchAnimationProgress(state, progress, linearProgress)
+ startController.onTransitionAnimationProgress(state, progress, linearProgress)
// The end view is visible only iff the starting view is not visible.
state.visible = !state.visible
- endController.onLaunchAnimationProgress(state, progress, linearProgress)
+ endController.onTransitionAnimationProgress(state, progress, linearProgress)
// If the dialog content is complex, its dimension might change during the
// launch animation. The animation end position might also change during the
@@ -973,7 +973,7 @@
}
}
- launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
+ transitionAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
}
private fun shouldAnimateDialogIntoSource(): Boolean {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
index 055252b..03f10f9 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt
@@ -66,11 +66,11 @@
) : ActivityLaunchAnimator.Controller {
/** The container to which we will add the ghost view and expanding background. */
- override var launchContainer = ghostedView.rootView as ViewGroup
- private val launchContainerOverlay: ViewGroupOverlay
- get() = launchContainer.overlay
+ override var transitionContainer = ghostedView.rootView as ViewGroup
+ private val transitionContainerOverlay: ViewGroupOverlay
+ get() = transitionContainer.overlay
- private val launchContainerLocation = IntArray(2)
+ private val transitionContainerLocation = IntArray(2)
/** The ghost view that is drawn and animated instead of the ghosted view. */
private var ghostView: GhostView? = null
@@ -78,8 +78,8 @@
private val ghostViewMatrix = Matrix()
/**
- * The expanding background view that will be added to [launchContainer] (below [ghostView]) and
- * animate.
+ * The expanding background view that will be added to [transitionContainer] (below [ghostView])
+ * and animate.
*/
private var backgroundView: FrameLayout? = null
@@ -92,7 +92,7 @@
private var startBackgroundAlpha: Int = 0xFF
private val ghostedViewLocation = IntArray(2)
- private val ghostedViewState = LaunchAnimator.State()
+ private val ghostedViewState = TransitionAnimator.State()
/**
* The background of the [ghostedView]. This background will be used to draw the background of
@@ -175,9 +175,9 @@
return radius * ghostedView.scaleX
}
- override fun createAnimatorState(): LaunchAnimator.State {
+ override fun createAnimatorState(): TransitionAnimator.State {
val state =
- LaunchAnimator.State(
+ TransitionAnimator.State(
topCornerRadius = getCurrentTopCornerRadius(),
bottomCornerRadius = getCurrentBottomCornerRadius()
)
@@ -185,7 +185,7 @@
return state
}
- fun fillGhostedViewState(state: LaunchAnimator.State) {
+ fun fillGhostedViewState(state: TransitionAnimator.State) {
// For the animation we are interested in the area that has a non transparent background,
// so we have to take the optical insets into account.
ghostedView.getLocationOnScreen(ghostedViewLocation)
@@ -200,7 +200,7 @@
insets.right
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
if (ghostedView.parent !is ViewGroup) {
// This should usually not happen, but let's make sure we don't crash if the view was
// detached right before we started the animation.
@@ -209,7 +209,7 @@
}
backgroundView =
- FrameLayout(launchContainer.context).also { launchContainerOverlay.add(it) }
+ FrameLayout(transitionContainer.context).also { transitionContainerOverlay.add(it) }
// We wrap the ghosted view background and use it to draw the expandable background. Its
// alpha will be set to 0 as soon as we start drawing the expanding background.
@@ -225,7 +225,7 @@
// Create a ghost of the view that will be moving and fading out. This allows to fade out
// the content before fading out the background.
- ghostView = GhostView.addGhost(ghostedView, launchContainer)
+ ghostView = GhostView.addGhost(ghostedView, transitionContainer)
// [GhostView.addGhost], the result of which is our [ghostView], creates a [GhostView], and
// adds it first to a [FrameLayout] container. It then adds _that_ container to an
@@ -244,8 +244,8 @@
cujType?.let { interactionJankMonitor.begin(ghostedView, it) }
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
@@ -287,15 +287,15 @@
if (ghostedView.parent is ViewGroup) {
// Recalculate the matrix in case the ghosted view moved. We ensure that the ghosted
// view is still attached to a ViewGroup, otherwise calculateMatrix will throw.
- GhostView.calculateMatrix(ghostedView, launchContainer, ghostViewMatrix)
+ GhostView.calculateMatrix(ghostedView, transitionContainer, ghostViewMatrix)
}
- launchContainer.getLocationOnScreen(launchContainerLocation)
+ transitionContainer.getLocationOnScreen(transitionContainerLocation)
ghostViewMatrix.postScale(
scale,
scale,
- ghostedViewState.centerX - launchContainerLocation[0],
- ghostedViewState.centerY - launchContainerLocation[1]
+ ghostedViewState.centerX - transitionContainerLocation[0],
+ ghostedViewState.centerY - transitionContainerLocation[1]
)
ghostViewMatrix.postTranslate(
(leftChange + rightChange) / 2f,
@@ -310,10 +310,10 @@
val rightWithInsets = state.right + insets.right
val bottomWithInsets = state.bottom + insets.bottom
- backgroundView.top = topWithInsets - launchContainerLocation[1]
- backgroundView.bottom = bottomWithInsets - launchContainerLocation[1]
- backgroundView.left = leftWithInsets - launchContainerLocation[0]
- backgroundView.right = rightWithInsets - launchContainerLocation[0]
+ backgroundView.top = topWithInsets - transitionContainerLocation[1]
+ backgroundView.bottom = bottomWithInsets - transitionContainerLocation[1]
+ backgroundView.left = leftWithInsets - transitionContainerLocation[0]
+ backgroundView.right = rightWithInsets - transitionContainerLocation[0]
val backgroundDrawable = backgroundDrawable!!
backgroundDrawable.wrapped?.let {
@@ -321,7 +321,7 @@
}
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
if (ghostView == null) {
// We didn't actually run the animation.
return
@@ -332,7 +332,7 @@
backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
GhostView.removeGhost(ghostedView)
- backgroundView?.let { launchContainerOverlay.remove(it) }
+ backgroundView?.let { transitionContainerOverlay.remove(it) }
if (ghostedView is LaunchableView) {
// Restore the ghosted view visibility.
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
similarity index 79%
rename from packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
rename to packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index d6eba2e..5e4276c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -31,17 +31,18 @@
import com.android.app.animation.Interpolators.LINEAR
import kotlin.math.roundToInt
-private const val TAG = "LaunchAnimator"
+private const val TAG = "TransitionAnimator"
-/** A base class to animate a window launch (activity or dialog) from a view . */
-class LaunchAnimator(private val timings: Timings, private val interpolators: Interpolators) {
+/** A base class to animate a window (activity or dialog) launch to or return from a view . */
+class TransitionAnimator(private val timings: Timings, private val interpolators: Interpolators) {
companion object {
internal const val DEBUG = false
private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
/**
- * Given the [linearProgress] of a launch animation, return the linear progress of the
- * sub-animation starting [delay] ms after the launch animation and that lasts [duration].
+ * Given the [linearProgress] of a transition animation, return the linear progress of the
+ * sub-animation starting [delay] ms after the transition animation and that lasts
+ * [duration].
*/
@JvmStatic
fun getProgress(
@@ -58,7 +59,7 @@
}
}
- private val launchContainerLocation = IntArray(2)
+ private val transitionContainerLocation = IntArray(2)
private val cornerRadii = FloatArray(8)
/**
@@ -73,7 +74,7 @@
*
* This will be used to:
* - Get the associated [Context].
- * - Compute whether we are expanding fully above the launch container.
+ * - Compute whether we are expanding fully above the transition container.
* - Get to overlay to which we initially put the window background layer, until the opening
* window is made visible (see [openingWindowSyncView]).
*
@@ -81,7 +82,7 @@
* inside a different location, for instance to ensure correct layering during the
* animation.
*/
- var launchContainer: ViewGroup
+ var transitionContainer: ViewGroup
/**
* The [View] with which the opening app window should be synchronized with once it starts
@@ -90,7 +91,7 @@
* We will also move the window background layer to this view's overlay once the opening
* window is visible.
*
- * If null, this will default to [launchContainer].
+ * If null, this will default to [transitionContainer].
*/
val openingWindowSyncView: View?
get() = null
@@ -99,7 +100,7 @@
* Return the [State] of the view that will be animated. We will animate from this state to
* the final window state.
*
- * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the
+ * Note: This state will be mutated and passed to [onTransitionAnimationProgress] during the
* animation.
*/
fun createAnimatorState(): State
@@ -107,22 +108,22 @@
/**
* The animation started. This is typically used to initialize any additional resource
* needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
- * fully above the [launchContainer].
+ * fully above the [transitionContainer].
*/
- fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
+ fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {}
/** The animation made progress and the expandable view [state] should be updated. */
- fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
+ fun onTransitionAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
/**
- * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was
- * called previously. This is typically used to clean up the resources initialized when the
- * animation was started.
+ * The animation ended. This will be called *if and only if* [onTransitionAnimationStart]
+ * was called previously. This is typically used to clean up the resources initialized when
+ * the animation was started.
*/
- fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
+ fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {}
}
- /** The state of an expandable view during a [LaunchAnimator] animation. */
+ /** The state of an expandable view during a [TransitionAnimator] animation. */
open class State(
/** The position of the view in screen space coordinates. */
var top: Int = 0,
@@ -198,13 +199,13 @@
)
/**
- * Start a launch animation controlled by [controller] towards [endState]. An intermediary layer
- * with [windowBackgroundColor] will fade in then (optionally) fade out above the expanding
- * view, and should be the same background color as the opening (or closing) window.
+ * Start a transition animation controlled by [controller] towards [endState]. An intermediary
+ * layer with [windowBackgroundColor] will fade in then (optionally) fade out above the
+ * expanding view, and should be the same background color as the opening (or closing) window.
*
* If [fadeOutWindowBackgroundLayer] is true, then this intermediary layer will fade out during
* the second half of the animation, and will have SRC blending mode (ultimately punching a hole
- * in the [launch container][Controller.launchContainer]) iff [drawHole] is true.
+ * in the [transition container][Controller.transitionContainer]) iff [drawHole] is true.
*/
fun startAnimation(
controller: Controller,
@@ -251,13 +252,13 @@
}
}
- val launchContainer = controller.launchContainer
- val isExpandingFullyAbove = isExpandingFullyAbove(launchContainer, endState)
+ val transitionContainer = controller.transitionContainer
+ val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState)
// We add an extra layer with the same color as the dialog/app splash screen background
// color, which is usually the same color of the app background. We first fade in this layer
// to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
- // launch container and reveal the opening window.
+ // transition container and reveal the opening window.
val windowBackgroundLayer =
GradientDrawable().apply {
setColor(windowBackgroundColor)
@@ -275,9 +276,9 @@
val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
val moveBackgroundLayerWhenAppIsVisible =
openingWindowSyncView != null &&
- openingWindowSyncView.viewRootImpl != controller.launchContainer.viewRootImpl
+ openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl
- val launchContainerOverlay = launchContainer.overlay
+ val transitionContainerOverlay = transitionContainer.overlay
var cancelled = false
var movedBackgroundLayer = false
@@ -287,20 +288,20 @@
if (DEBUG) {
Log.d(TAG, "Animation started")
}
- controller.onLaunchAnimationStart(isExpandingFullyAbove)
+ controller.onTransitionAnimationStart(isExpandingFullyAbove)
- // Add the drawable to the launch container overlay. Overlays always draw
+ // Add the drawable to the transition container overlay. Overlays always draw
// drawables after views, so we know that it will be drawn above any view added
// by the controller.
- launchContainerOverlay.add(windowBackgroundLayer)
+ transitionContainerOverlay.add(windowBackgroundLayer)
}
override fun onAnimationEnd(animation: Animator) {
if (DEBUG) {
Log.d(TAG, "Animation ended")
}
- controller.onLaunchAnimationEnd(isExpandingFullyAbove)
- launchContainerOverlay.remove(windowBackgroundLayer)
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+ transitionContainerOverlay.remove(windowBackgroundLayer)
if (moveBackgroundLayerWhenAppIsVisible) {
openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
@@ -353,17 +354,21 @@
// in its new container.
movedBackgroundLayer = true
- launchContainerOverlay.remove(windowBackgroundLayer)
+ transitionContainerOverlay.remove(windowBackgroundLayer)
openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)
- ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {})
+ ViewRootSync.synchronizeNextDraw(
+ transitionContainer,
+ openingWindowSyncView,
+ then = {}
+ )
}
val container =
if (movedBackgroundLayer) {
openingWindowSyncView!!
} else {
- controller.launchContainer
+ controller.transitionContainer
}
applyStateToWindowBackgroundLayer(
@@ -374,7 +379,7 @@
fadeOutWindowBackgroundLayer,
drawHole
)
- controller.onLaunchAnimationProgress(state, progress, linearProgress)
+ controller.onTransitionAnimationProgress(state, progress, linearProgress)
}
animator.start()
@@ -386,30 +391,30 @@
}
}
- /** Return whether we are expanding fully above the [launchContainer]. */
- internal fun isExpandingFullyAbove(launchContainer: View, endState: State): Boolean {
- launchContainer.getLocationOnScreen(launchContainerLocation)
- return endState.top <= launchContainerLocation[1] &&
- endState.bottom >= launchContainerLocation[1] + launchContainer.height &&
- endState.left <= launchContainerLocation[0] &&
- endState.right >= launchContainerLocation[0] + launchContainer.width
+ /** Return whether we are expanding fully above the [transitionContainer]. */
+ internal fun isExpandingFullyAbove(transitionContainer: View, endState: State): Boolean {
+ transitionContainer.getLocationOnScreen(transitionContainerLocation)
+ return endState.top <= transitionContainerLocation[1] &&
+ endState.bottom >= transitionContainerLocation[1] + transitionContainer.height &&
+ endState.left <= transitionContainerLocation[0] &&
+ endState.right >= transitionContainerLocation[0] + transitionContainer.width
}
private fun applyStateToWindowBackgroundLayer(
drawable: GradientDrawable,
state: State,
linearProgress: Float,
- launchContainer: View,
+ transitionContainer: View,
fadeOutWindowBackgroundLayer: Boolean,
drawHole: Boolean
) {
// Update position.
- launchContainer.getLocationOnScreen(launchContainerLocation)
+ transitionContainer.getLocationOnScreen(transitionContainerLocation)
drawable.setBounds(
- state.left - launchContainerLocation[0],
- state.top - launchContainerLocation[1],
- state.right - launchContainerLocation[0],
- state.bottom - launchContainerLocation[1]
+ state.left - transitionContainerLocation[0],
+ state.top - transitionContainerLocation[1],
+ state.right - transitionContainerLocation[0],
+ state.bottom - transitionContainerLocation[1]
)
// Update radius.
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
index 1290f00..e2a29ab 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
@@ -68,19 +68,19 @@
}
}
- override fun createLaunchController(): LaunchAnimator.Controller {
+ override fun createTransitionController(): TransitionAnimator.Controller {
val delegate = GhostedViewLaunchAnimatorController(source)
- return object : LaunchAnimator.Controller by delegate {
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ return object : TransitionAnimator.Controller by delegate {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
// Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
// ghost (that ghosts only the source content, and not its background) will
// be added right after this by the delegate and will be animated.
GhostView.removeGhost(source)
- delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
// At this point the view visibility is restored by the delegate, so we delay the
// visibility changes again and make it invisible while the dialog is shown.
@@ -94,7 +94,7 @@
}
}
- override fun createExitController(): LaunchAnimator.Controller {
+ override fun createExitController(): TransitionAnimator.Controller {
return GhostedViewLaunchAnimatorController(source)
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
index ac1ef15..8eb2f2e 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt
@@ -75,7 +75,7 @@
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import com.android.systemui.animation.Expandable
-import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.animation.TransitionAnimator
import kotlin.math.max
import kotlin.math.min
@@ -301,7 +301,7 @@
private fun AnimatedContentInOverlay(
color: Color,
sizeInOriginalLayout: Size,
- animatorState: State<LaunchAnimator.State?>,
+ animatorState: State<TransitionAnimator.State?>,
overlay: ViewGroupOverlay,
controller: ExpandableControllerImpl,
content: @Composable (Expandable) -> Unit,
@@ -407,7 +407,7 @@
internal fun measureAndLayoutComposeViewInOverlay(
view: View,
- state: LaunchAnimator.State,
+ state: TransitionAnimator.State,
) {
val exactWidth = state.width
val exactHeight = state.height
@@ -449,7 +449,7 @@
}
private fun ContentDrawScope.drawBackground(
- animatorState: LaunchAnimator.State,
+ animatorState: TransitionAnimator.State,
color: Color,
border: BorderStroke?,
) {
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
index 0e7694e..1020263 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
@@ -44,7 +44,7 @@
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.animation.Expandable
-import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.animation.TransitionAnimator
import kotlin.math.roundToInt
/** A controller that can control animated launches from an [Expandable]. */
@@ -70,7 +70,7 @@
val layoutDirection = LocalLayoutDirection.current
// The current animation state, if we are currently animating a dialog or activity.
- val animatorState = remember { mutableStateOf<LaunchAnimator.State?>(null) }
+ val animatorState = remember { mutableStateOf<TransitionAnimator.State?>(null) }
// Whether a dialog controlled by this ExpandableController is currently showing.
val isDialogShowing = remember { mutableStateOf(false) }
@@ -123,7 +123,7 @@
internal val borderStroke: BorderStroke?,
internal val composeViewRoot: View,
internal val density: Density,
- internal val animatorState: MutableState<LaunchAnimator.State?>,
+ internal val animatorState: MutableState<TransitionAnimator.State?>,
internal val isDialogShowing: MutableState<Boolean>,
internal val overlay: MutableState<ViewGroupOverlay?>,
internal val currentComposeViewInOverlay: MutableState<View?>,
@@ -153,32 +153,32 @@
}
/**
- * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog
- * animation. This controller will:
+ * Create a [TransitionAnimator.Controller] that is going to be used to drive an activity or
+ * dialog animation. This controller will:
* 1. Compute the start/end animation state using [boundsInComposeViewRoot] and the location of
* composeViewRoot on the screen.
* 2. Update [animatorState] with the current animation state if we are animating, or null
* otherwise.
*/
- private fun launchController(): LaunchAnimator.Controller {
- return object : LaunchAnimator.Controller {
+ private fun transitionController(): TransitionAnimator.Controller {
+ return object : TransitionAnimator.Controller {
private val rootLocationOnScreen = intArrayOf(0, 0)
- override var launchContainer: ViewGroup = composeViewRoot.rootView as ViewGroup
+ override var transitionContainer: ViewGroup = composeViewRoot.rootView as ViewGroup
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
animatorState.value = null
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
// We copy state given that it's always the same object that is mutated by
// ActivityLaunchAnimator.
animatorState.value =
- LaunchAnimator.State(
+ TransitionAnimator.State(
state.top,
state.bottom,
state.left,
@@ -195,7 +195,7 @@
}
}
- override fun createAnimatorState(): LaunchAnimator.State {
+ override fun createAnimatorState(): TransitionAnimator.State {
val boundsInRoot = boundsInComposeViewRoot.value
val outline =
shape.createOutline(
@@ -236,7 +236,7 @@
}
val rootLocation = rootLocationOnScreen()
- return LaunchAnimator.State(
+ return TransitionAnimator.State(
top = rootLocation.y.roundToInt(),
bottom = (rootLocation.y + boundsInRoot.height).roundToInt(),
left = rootLocation.x.roundToInt(),
@@ -258,17 +258,18 @@
/** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */
private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller {
- val delegate = launchController()
- return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate {
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+ val delegate = transitionController()
+ return object :
+ ActivityLaunchAnimator.Controller, TransitionAnimator.Controller by delegate {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
overlay.value = composeViewRoot.rootView.overlay as ViewGroupOverlay
cujType?.let { InteractionJankMonitor.getInstance().begin(composeViewRoot, it) }
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
cujType?.let { InteractionJankMonitor.getInstance().end(it) }
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
overlay.value = null
}
}
@@ -293,11 +294,11 @@
}
}
- override fun createLaunchController(): LaunchAnimator.Controller {
- val delegate = launchController()
- return object : LaunchAnimator.Controller by delegate {
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ override fun createTransitionController(): TransitionAnimator.Controller {
+ val delegate = transitionController()
+ return object : TransitionAnimator.Controller by delegate {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
// Make sure we don't draw this expandable when the dialog is showing.
isDialogShowing.value = true
@@ -305,11 +306,11 @@
}
}
- override fun createExitController(): LaunchAnimator.Controller {
- val delegate = launchController()
- return object : LaunchAnimator.Controller by delegate {
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ override fun createExitController(): TransitionAnimator.Controller {
+ val delegate = transitionController()
+ return object : TransitionAnimator.Controller by delegate {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
isDialogShowing.value = false
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Color.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Color.kt
new file mode 100644
index 0000000..64b9f2d
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Color.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.common.ui.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.graphics.Color
+import com.android.compose.theme.colorAttr
+
+/** Resolves [com.android.systemui.common.shared.model.Color] into [Color] */
+@Composable
+@ReadOnlyComposable
+fun com.android.systemui.common.shared.model.Color.toColor(): Color {
+ return when (this) {
+ is com.android.systemui.common.shared.model.Color.Attribute -> colorAttr(attribute)
+ is com.android.systemui.common.shared.model.Color.Loaded -> Color(color)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index d70f82f..ef6ae2e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -19,6 +19,7 @@
import android.util.Log
import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
@@ -140,6 +141,8 @@
) {
val density = LocalDensity.current
val screenCornerRadius = LocalScreenCornerRadius.current
+ val scrollState = rememberScrollState()
+ val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f)
val expansionFraction by viewModel.expandFraction.collectAsState(0f)
val navBarHeight =
@@ -180,11 +183,28 @@
// if contentHeight drops below minimum visible scrim height while scrim is
// expanded, reset scrim offset.
- LaunchedEffect(contentHeight, screenHeight, maxScrimTop, scrimOffset) {
+ LaunchedEffect(contentHeight, scrimOffset) {
snapshotFlow { contentHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f }
.collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f }
}
+ // if we receive scroll delta from NSSL, offset the scrim and placeholder accordingly.
+ LaunchedEffect(syntheticScroll, scrimOffset, scrollState) {
+ snapshotFlow { syntheticScroll.value }
+ .collect { delta ->
+ val minOffset = minScrimOffset()
+ if (scrimOffset.value > minOffset) {
+ val remainingDelta = (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f)
+ scrimOffset.value = (scrimOffset.value - delta).coerceAtLeast(minOffset)
+ if (remainingDelta > 0f) {
+ scrollState.scrollBy(remainingDelta)
+ }
+ } else {
+ scrollState.scrollTo(delta.roundToInt())
+ }
+ }
+ }
+
Box(
modifier =
modifier
@@ -260,7 +280,7 @@
)
}
)
- .verticalScroll(rememberScrollState())
+ .verticalScroll(scrollState)
.fillMaxWidth()
.height { (contentHeight.value + navBarHeight).roundToInt() },
)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index c027c49..de8f2ec 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -18,21 +18,21 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.MovableElementScenePicker
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.TransitionState
+import com.android.compose.modifiers.thenIf
import com.android.compose.theme.colorAttr
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
@@ -44,9 +44,16 @@
import com.android.systemui.scene.ui.composable.Shade
object QuickSettings {
+ private val SCENES =
+ setOf(
+ QuickSettingsSceneKey,
+ Shade,
+ )
+
object Elements {
// TODO RENAME
- val Content = ElementKey("QuickSettingsContent")
+ val Content =
+ ElementKey("QuickSettingsContent", scenePicker = MovableElementScenePicker(SCENES))
val CollapsedGrid = ElementKey("QuickSettingsCollapsedGrid")
val FooterActions = ElementKey("QuickSettingsFooterActions")
}
@@ -86,14 +93,22 @@
*/
@Composable
fun SceneScope.QuickSettings(
- modifier: Modifier = Modifier,
qsSceneAdapter: QSSceneAdapter,
+ heightProvider: () -> Int,
+ modifier: Modifier = Modifier,
) {
val contentState = stateForQuickSettingsContent()
MovableElement(
key = QuickSettings.Elements.Content,
- modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp)
+ modifier =
+ modifier.fillMaxWidth().layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ // Use the height of the correct view based on the scene it is being composed in
+ val height = heightProvider()
+
+ layout(placeable.width, height) { placeable.placeRelative(0, 0) }
+ }
) {
content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) }
}
@@ -118,15 +133,7 @@
qsView?.let { view ->
Box(
modifier =
- modifier
- .fillMaxWidth()
- .then(
- if (isCustomizing) {
- Modifier.fillMaxHeight()
- } else {
- Modifier.wrapContentHeight()
- }
- )
+ modifier.fillMaxWidth().thenIf(isCustomizing) { Modifier.fillMaxHeight() }
) {
AndroidView(
modifier = Modifier.fillMaxWidth().background(colorAttr(R.attr.underSurface)),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 969dec3..1cbc992 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -213,8 +213,9 @@
Spacer(modifier = Modifier.height(16.dp))
// This view has its own horizontal padding
QuickSettings(
- modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"),
viewModel.qsSceneAdapter,
+ { viewModel.qsSceneAdapter.qsHeight },
+ modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"),
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 9f9e1f5..da1b417 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -40,6 +40,7 @@
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction as SceneTransitionUserAction
+import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.observableTransitionState
import com.android.compose.animation.scene.updateSceneTransitionLayoutState
import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon
@@ -168,7 +169,7 @@
private fun toTransitionModels(
userAction: UserAction,
sceneModel: SceneModel,
-): Pair<SceneTransitionUserAction, SceneTransitionSceneKey> {
+): Pair<SceneTransitionUserAction, UserActionResult> {
return userAction.toTransitionUserAction() to sceneModel.key.toTransitionSceneKey()
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 677df7e..cac35cb 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -189,8 +189,8 @@
)
)
QuickSettings(
- modifier = Modifier.height(130.dp),
viewModel.qsSceneAdapter,
+ { viewModel.qsSceneAdapter.qqsHeight },
)
if (viewModel.isMediaVisible()) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index 2596d4a..9770399 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -44,7 +44,7 @@
class SceneKey(
debugName: String,
identity: Any = Object(),
-) : Key(debugName, identity), UserActionResult {
+) : Key(debugName, identity) {
@VisibleForTesting
// TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
// access internal members.
@@ -53,11 +53,6 @@
/** The unique [ElementKey] identifying this scene's root element. */
val rootElementKey = ElementKey(debugName, identity)
- // Implementation of [UserActionResult].
- override val toScene: SceneKey = this
- override val transitionKey: TransitionKey? = null
- override val distance: UserActionDistance? = null
-
override fun toString(): String {
return "SceneKey(debugName=$debugName)"
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index d904c8b..e1f8a09 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -332,7 +332,11 @@
@Stable @ElementDsl interface MovableElementContentScope : BaseSceneScope, ElementBoxScope
/** An action performed by the user. */
-sealed interface UserAction
+sealed interface UserAction {
+ infix fun to(scene: SceneKey): Pair<UserAction, UserActionResult> {
+ return this to UserActionResult(toScene = scene)
+ }
+}
/** The user navigated back, either using a gesture or by triggering a KEYCODE_BACK event. */
data object Back : UserAction
@@ -385,65 +389,26 @@
): SwipeSource?
}
-/**
- * The result of performing a [UserAction].
- *
- * Note: [UserActionResult] is implemented by [SceneKey], so you can also use scene keys directly
- * when defining your [UserActionResult]s.
- *
- * ```
- * SceneTransitionLayout(...) {
- * scene(
- * Scenes.Foo,
- * userActions =
- * mapOf(
- * Swipe.Right to Scene.Bar,
- * Swipe.Down to Scene.Doe,
- * )
- * )
- * ) { ... }
- * }
- * ```
- */
-interface UserActionResult {
+/** The result of performing a [UserAction]. */
+class UserActionResult(
/** The scene we should be transitioning to during the [UserAction]. */
- val toScene: SceneKey
-
- /** The key of the transition that should be used. */
- val transitionKey: TransitionKey?
+ val toScene: SceneKey,
/**
* The distance the action takes to animate from 0% to 100%.
*
* If `null`, a default distance will be used that depends on the [UserAction] performed.
*/
- val distance: UserActionDistance?
-}
+ val distance: UserActionDistance? = null,
-/** Create a [UserActionResult] to [toScene] with the given [distance] and [transitionKey]. */
-fun UserActionResult(
- toScene: SceneKey,
- distance: UserActionDistance? = null,
- transitionKey: TransitionKey? = null,
-): UserActionResult {
- return object : UserActionResult {
- override val toScene: SceneKey = toScene
- override val transitionKey: TransitionKey? = transitionKey
- override val distance: UserActionDistance? = distance
- }
-}
-
-/** Create a [UserActionResult] to [toScene] with the given fixed [distance] and [transitionKey]. */
-fun UserActionResult(
- toScene: SceneKey,
- distance: Dp,
- transitionKey: TransitionKey? = null,
-): UserActionResult {
- return UserActionResult(
- toScene = toScene,
- distance = FixedDistance(distance),
- transitionKey = transitionKey,
- )
+ /** The key of the transition that should be used. */
+ val transitionKey: TransitionKey? = null,
+) {
+ constructor(
+ toScene: SceneKey,
+ distance: Dp,
+ transitionKey: TransitionKey? = null,
+ ) : this(toScene, FixedDistance(distance), transitionKey)
}
interface UserActionDistance {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 2dc94a4..dacbdb4 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -54,12 +54,8 @@
private val layoutState =
MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions)
- val mutableUserActionsA: MutableMap<UserAction, SceneKey> =
- mutableMapOf(Swipe.Up to SceneB, Swipe.Down to SceneC)
-
- val mutableUserActionsB: MutableMap<UserAction, SceneKey> =
- mutableMapOf(Swipe.Up to SceneC, Swipe.Down to SceneA)
-
+ val mutableUserActionsA = mutableMapOf(Swipe.Up to SceneB, Swipe.Down to SceneC)
+ val mutableUserActionsB = mutableMapOf(Swipe.Up to SceneC, Swipe.Down to SceneA)
private val scenesBuilder: SceneTransitionLayoutScope.() -> Unit = {
scene(
key = SceneA,
@@ -507,7 +503,7 @@
onDragStarted(overSlop = up(fractionOfScreen = 0.1f))
assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f)
- mutableUserActionsA[Swipe.Up] = SceneC
+ mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC)
onDelta(pixels = up(fractionOfScreen = 0.1f))
// target stays B even though UserActions changed
assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.2f)
@@ -524,7 +520,7 @@
onDragStarted(overSlop = up(fractionOfScreen = 0.1f))
assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f)
- mutableUserActionsA[Swipe.Up] = SceneC
+ mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC)
onDelta(pixels = up(fractionOfScreen = 0.1f))
onDragStopped(velocity = down(fractionOfScreen = 0.1f))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index c4bcb53..f86342c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -31,6 +31,7 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.classifier.FalsingCollectorFake;
@@ -102,13 +103,15 @@
.thenReturn(mOkButton);
when(mPinBasedInputView.getResources()).thenReturn(getContext().getResources());
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor =
+ new KeyguardKeyboardInteractor(new FakeKeyboardRepository());
FakeFeatureFlags featureFlags = new FakeFeatureFlags();
mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_REVAMPED_BOUNCER_MESSAGES);
mKeyguardPinViewController = new KeyguardPinBasedInputViewController(mPinBasedInputView,
mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener,
mEmergencyButtonController, mFalsingCollector, featureFlags,
- mSelectedUserInteractor, new FakeKeyboardRepository()) {
+ mSelectedUserInteractor, keyguardKeyboardInteractor) {
@Override
public void onResume(int reason) {
super.onResume(reason);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
index ea766f8..805b4a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
@@ -26,7 +26,6 @@
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -75,7 +74,7 @@
fun start_afterStop_startsTheTrackingAgain() = runOnStartedPlugin {
// WHEN the plugin is restarted
plugin.stop()
- plugin.start()
+ plugin.startInScope(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// THEN the tracking begins again
assertThat(plugin.isTracking).isTrue()
@@ -131,22 +130,21 @@
private fun runOnStartedPlugin(test: suspend TestScope.() -> Unit) =
with(kosmos) {
testScope.runTest {
- createPlugin(this, UnconfinedTestDispatcher(testScheduler))
- // GIVEN that the plugin is started
- plugin.start()
+ val pluginScope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))
+ createPlugin()
+ // GIVEN that the plugin is started in a test scope
+ plugin.startInScope(pluginScope)
// THEN run the test
test()
}
}
- private fun createPlugin(scope: CoroutineScope, dispatcher: CoroutineDispatcher) {
+ private fun createPlugin() {
plugin =
SeekableSliderHapticPlugin(
vibratorHelper,
kosmos.fakeSystemClock,
- dispatcher,
- scope,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
index 0543bc2..d52696a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
@@ -20,6 +20,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -68,6 +69,8 @@
@Before
fun setUp() {
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
+
MockitoAnnotations.initMocks(this)
whenever(burnInInteractor.keyguardBurnIn).thenReturn(burnInFlow)
kosmos.burnInInteractor = burnInInteractor
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 2de013b..c23ec22 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -116,6 +116,23 @@
}
@Test
+ fun iconContainer_isNotVisible_onKeyguard_dontShowWhenGoneToAodTransitionRunning() =
+ testScope.runTest {
+ val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
+ runCurrent()
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ testScope,
+ )
+ whenever(screenOffAnimationController.shouldShowAodIconsWhenShade()).thenReturn(false)
+ runCurrent()
+
+ assertThat(isVisible?.value).isFalse()
+ assertThat(isVisible?.isAnimating).isFalse()
+ }
+
+ @Test
fun iconContainer_isVisible_bypassEnabled() =
testScope.runTest {
val isVisible by collectLastValue(underTest.isNotifIconContainerVisible)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index 42200a3..51f8b11 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -61,7 +61,7 @@
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
- private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() }
+ private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
private val footerActionsViewModel = mock<FooterActionsViewModel>()
private val footerActionsViewModelFactory =
mock<FooterActionsViewModel.Factory> {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 5ef095f..f1f5dc3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -82,7 +82,7 @@
scope = testScope.backgroundScope,
)
- private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() }
+ private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
diff --git a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
index dec9930..a80d3b4 100644
--- a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
+++ b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
@@ -20,6 +20,7 @@
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
+ android:alpha="0.3"
>
<path
android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 1b71256..15688c5 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1599,6 +1599,15 @@
<!-- Accessibility label for hotspot icon [CHAR LIMIT=NONE] -->
<string name="accessibility_status_bar_hotspot">Hotspot</string>
+ <!-- Accessibility label for no satellite connection [CHAR LIMIT=NONE] -->
+ <string name="accessibility_status_bar_satellite_no_connection">Satellite, no connection</string>
+ <!-- Accessibility label for poor satellite connection [CHAR LIMIT=NONE] -->
+ <string name="accessibility_status_bar_satellite_poor_connection">Satellite, poor connection</string>
+ <!-- Accessibility label for good satellite connection [CHAR LIMIT=NONE] -->
+ <string name="accessibility_status_bar_satellite_good_connection">Satellite, good connection</string>
+ <!-- Accessibility label for available satellite connection [CHAR LIMIT=NONE] -->
+ <string name="accessibility_status_bar_satellite_available">Satellite, connection available</string>
+
<!-- Accessibility label for managed profile icon (not shown on screen) [CHAR LIMIT=NONE] -->
<string name="accessibility_managed_profile">Work profile</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 1a10c7a..458a21c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -38,7 +38,6 @@
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;
@@ -212,7 +211,6 @@
private final FeatureFlags mFeatureFlags;
private final SelectedUserInteractor mSelectedUserInteractor;
private final UiEventLogger mUiEventLogger;
- private final KeyboardRepository mKeyboardRepository;
private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor;
@Inject
@@ -228,7 +226,6 @@
KeyguardViewController keyguardViewController,
FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor,
UiEventLogger uiEventLogger,
- KeyboardRepository keyboardRepository,
KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mLockPatternUtils = lockPatternUtils;
@@ -246,7 +243,6 @@
mFeatureFlags = featureFlags;
mSelectedUserInteractor = selectedUserInteractor;
mUiEventLogger = uiEventLogger;
- mKeyboardRepository = keyboardRepository;
mKeyguardKeyboardInteractor = keyguardKeyboardInteractor;
}
@@ -277,7 +273,7 @@
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, emergencyButtonController, mFalsingCollector,
mDevicePostureController, mFeatureFlags, mSelectedUserInteractor,
- mUiEventLogger, mKeyboardRepository
+ mUiEventLogger, mKeyguardKeyboardInteractor
);
} else if (keyguardInputView instanceof KeyguardSimPinView) {
return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView,
@@ -285,14 +281,15 @@
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, mTelephonyManager, mFalsingCollector,
emergencyButtonController, mFeatureFlags, mSelectedUserInteractor,
- mKeyboardRepository);
+ mKeyguardKeyboardInteractor);
} else if (keyguardInputView instanceof KeyguardSimPukView) {
return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mLiftToActivateListener, mTelephonyManager, mFalsingCollector,
emergencyButtonController, mFeatureFlags, mSelectedUserInteractor,
- mKeyboardRepository);
+ mKeyguardKeyboardInteractor
+ );
}
throw new RuntimeException("Unable to find controller for " + keyguardInputView);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 60dd568..476497d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -16,8 +16,8 @@
package com.android.keyguard;
-import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import static com.android.systemui.Flags.pinInputFieldStyledFocusState;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
@@ -32,9 +32,9 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
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;
@@ -43,7 +43,7 @@
private final LiftToActivateListener mLiftToActivateListener;
private final FalsingCollector mFalsingCollector;
- private final KeyboardRepository mKeyboardRepository;
+ private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor;
protected PasswordTextView mPasswordEntry;
private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> {
@@ -75,13 +75,13 @@
FalsingCollector falsingCollector,
FeatureFlags featureFlags,
SelectedUserInteractor selectedUserInteractor,
- KeyboardRepository keyboardRepository) {
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, falsingCollector,
emergencyButtonController, featureFlags, selectedUserInteractor);
mLiftToActivateListener = liftToActivateListener;
mFalsingCollector = falsingCollector;
- mKeyboardRepository = keyboardRepository;
+ mKeyguardKeyboardInteractor = keyguardKeyboardInteractor;
mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId());
}
@@ -132,7 +132,7 @@
okButton.setOnHoverListener(mLiftToActivateListener);
}
if (pinInputFieldStyledFocusState()) {
- collectFlow(mPasswordEntry, mKeyboardRepository.isAnyKeyboardConnected(),
+ collectFlow(mPasswordEntry, mKeyguardKeyboardInteractor.isAnyKeyboardConnected(),
this::setKeyboardBasedFocusOutline);
/**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index b958f55..f4cda02 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -25,10 +25,10 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
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;
@@ -61,11 +61,11 @@
FalsingCollector falsingCollector,
DevicePostureController postureController, FeatureFlags featureFlags,
SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger,
- KeyboardRepository keyboardRepository) {
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyboardRepository);
+ keyguardKeyboardInteractor);
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 1cdcbd0..558679e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
@@ -42,9 +42,9 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
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;
@@ -94,11 +94,12 @@
LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener,
TelephonyManager telephonyManager, FalsingCollector falsingCollector,
EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags,
- SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) {
+ SelectedUserInteractor selectedUserInteractor,
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyboardRepository);
+ keyguardKeyboardInteractor);
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 f019d61..cb1c4b3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
@@ -37,9 +37,9 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
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;
@@ -91,11 +91,12 @@
LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener,
TelephonyManager telephonyManager, FalsingCollector falsingCollector,
EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags,
- SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) {
+ SelectedUserInteractor selectedUserInteractor,
+ KeyguardKeyboardInteractor keyguardKeyboardInteractor) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, liftToActivateListener,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyboardRepository);
+ keyguardKeyboardInteractor);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);
diff --git a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
index 7a560e8..29df49b5 100644
--- a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
+++ b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt
@@ -29,26 +29,28 @@
import javax.inject.Inject
/**
- * Coordinates screen on/turning on animations for the KeyguardViewMediator. Specifically for
- * screen on events, this will invoke the onDrawn Runnable after all tasks have completed. This
- * should route back to the [com.android.systemui.keyguard.KeyguardService], which informs
- * the system_server that keyguard has drawn.
+ * Coordinates screen on/turning on animations for the KeyguardViewMediator. Specifically for screen
+ * on events, this will invoke the onDrawn Runnable after all tasks have completed. This should
+ * route back to the [com.android.systemui.keyguard.KeyguardService], which informs the
+ * system_server that keyguard has drawn.
*/
@SysUISingleton
-class ScreenOnCoordinator @Inject constructor(
+class ScreenOnCoordinator
+@Inject
+constructor(
unfoldComponent: Optional<SysUIUnfoldComponent>,
- @Main private val mainHandler: Handler
+ @Main private val mainHandler: Handler,
) {
- private val unfoldLightRevealAnimation = unfoldComponent.map(
- SysUIUnfoldComponent::getUnfoldLightRevealOverlayAnimation).getOrNull()
- private val foldAodAnimationController = unfoldComponent.map(
- SysUIUnfoldComponent::getFoldAodAnimationController).getOrNull()
+ private val foldAodAnimationController =
+ unfoldComponent.map(SysUIUnfoldComponent::getFoldAodAnimationController).getOrNull()
+ private val fullScreenLightRevealAnimations =
+ unfoldComponent.map(SysUIUnfoldComponent::getFullScreenLightRevealAnimations).getOrNull()
private val pendingTasks = PendingTasksContainer()
/**
- * When turning on, registers tasks that may need to run before invoking [onDrawn].
- * This is called on a binder thread from [com.android.systemui.keyguard.KeyguardService].
+ * When turning on, registers tasks that may need to run before invoking [onDrawn]. This is
+ * called on a binder thread from [com.android.systemui.keyguard.KeyguardService].
*/
@BinderThread
fun onScreenTurningOn(onDrawn: Runnable) {
@@ -56,8 +58,10 @@
pendingTasks.reset()
- unfoldLightRevealAnimation?.onScreenTurningOn(pendingTasks.registerTask("unfold-reveal"))
foldAodAnimationController?.onScreenTurningOn(pendingTasks.registerTask("fold-to-aod"))
+ fullScreenLightRevealAnimations?.forEach {
+ it.onScreenTurningOn(pendingTasks.registerTask(it::class.java.simpleName))
+ }
pendingTasks.onTasksComplete {
if (Flags.enableBackgroundKeyguardOndrawnCallback()) {
@@ -71,8 +75,8 @@
}
/**
- * Called when screen is fully turned on and screen on blocker is removed.
- * This is called on a binder thread from [com.android.systemui.keyguard.KeyguardService].
+ * Called when screen is fully turned on and screen on blocker is removed. This is called on a
+ * binder thread from [com.android.systemui.keyguard.KeyguardService].
*/
@BinderThread
fun onScreenTurnedOn() {
diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
index 4c9782c..39e1c41 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -36,7 +36,7 @@
* If your CoreStartable depends on different CoreStartables starting before it, use a
* {@link com.android.systemui.startable.Dependencies} annotation to list out those dependencies.
*
- * @see SystemUIApplication#startServicesIfNeeded()
+ * @see SystemUIApplication#startSystemUserServicesIfNeeded()
*/
public interface CoreStartable extends Dumpable {
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt
index e88aaf01..aab0b1e 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt
@@ -22,7 +22,6 @@
import android.content.ContentProvider
import android.content.Context
import android.content.Intent
-import android.util.Log
import androidx.core.app.AppComponentFactory
import com.android.systemui.dagger.ContextComponentHelper
import com.android.systemui.dagger.SysUIComponent
@@ -91,7 +90,8 @@
return app
}
- @UsesReflection(KeepTarget(instanceOfClassConstant = 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) {
@@ -103,11 +103,12 @@
.getMethod("inject", contentProvider.javaClass)
injectMethod.invoke(rootComponent, contentProvider)
} catch (e: NoSuchMethodException) {
- Log.w(TAG, "No injector for class: " + contentProvider.javaClass, e)
+ throw RuntimeException("No injector for class: " + contentProvider.javaClass, e)
} catch (e: IllegalAccessException) {
- Log.w(TAG, "No injector for class: " + contentProvider.javaClass, e)
+ throw RuntimeException("Injector inaccessible for class: " +
+ contentProvider.javaClass, e)
} catch (e: InvocationTargetException) {
- Log.w(TAG, "No injector for class: " + contentProvider.javaClass, e)
+ throw RuntimeException("Error while injecting: " + contentProvider.javaClass, e)
}
initializer
}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 8aae206..15ef61e 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -42,6 +42,7 @@
import com.android.systemui.dagger.GlobalRootComponent;
import com.android.systemui.dagger.SysUIComponent;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.process.ProcessWrapper;
import com.android.systemui.res.R;
import com.android.systemui.startable.Dependencies;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -77,6 +78,7 @@
private SystemUIAppComponentFactoryBase.ContextAvailableCallback mContextAvailableCallback;
private SysUIComponent mSysUIComponent;
private SystemUIInitializer mInitializer;
+ private ProcessWrapper mProcessWrapper;
public SystemUIApplication() {
super();
@@ -115,6 +117,7 @@
// Enable Looper trace points.
// This allows us to see Handler callbacks on traces.
rootComponent.getMainLooper().setTraceTag(Trace.TRACE_TAG_APP);
+ mProcessWrapper = rootComponent.getProcessWrapper();
// Set the application theme that is inherited by all services. Note that setting the
// application theme in the manifest does only work for activities. Keep this in sync with
@@ -132,7 +135,7 @@
View.setTraceLayoutSteps(true);
}
- if (rootComponent.getProcessWrapper().isSystemUser()) {
+ if (mProcessWrapper.isSystemUser()) {
IntentFilter bootCompletedFilter = new
IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED);
bootCompletedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
@@ -199,7 +202,11 @@
* <p>This method must only be called from the main thread.</p>
*/
- public void startServicesIfNeeded() {
+ public void startSystemUserServicesIfNeeded() {
+ if (!mProcessWrapper.isSystemUser()) {
+ Log.wtf(TAG, "Tried starting SystemUser services on non-SystemUser");
+ return; // Per-user startables are handled in #startSystemUserServicesIfNeeded.
+ }
final String vendorComponent = mInitializer.getVendorComponent(getResources());
// Sort the startables so that we get a deterministic ordering.
@@ -219,6 +226,9 @@
* <p>This method must only be called from the main thread.</p>
*/
void startSecondaryUserServicesIfNeeded() {
+ if (mProcessWrapper.isSystemUser()) {
+ return; // Per-user startables are handled in #startSystemUserServicesIfNeeded.
+ }
// Sort the startables so that we get a deterministic ordering.
Map<Class<?>, Provider<CoreStartable>> sortedStartables = new TreeMap<>(
Comparator.comparing(Class::getName));
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index 872b005..1a9b01f 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -22,10 +22,10 @@
import android.os.HandlerThread;
import android.util.Log;
-import com.android.systemui.res.R;
import com.android.systemui.dagger.GlobalRootComponent;
import com.android.systemui.dagger.SysUIComponent;
import com.android.systemui.dagger.WMComponent;
+import com.android.systemui.res.R;
import com.android.systemui.util.InitializationChecker;
import com.android.wm.shell.dagger.WMShellConcurrencyModule;
import com.android.wm.shell.keyguard.KeyguardTransitions;
@@ -124,9 +124,6 @@
.setDesktopMode(Optional.ofNullable(null));
}
mSysUIComponent = builder.build();
- if (initializeComponents) {
- mSysUIComponent.init();
- }
// Every other part of our codebase currently relies on Dependency, so we
// really need to ensure the Dependency gets initialized early on.
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java b/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java
index f4ec6f7..407f764 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java
@@ -19,12 +19,31 @@
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
+import android.util.Log;
+
+import com.android.systemui.process.ProcessWrapper;
+
+import javax.inject.Inject;
public class SystemUISecondaryUserService extends Service {
+ private static final String TAG = "SysUISecondaryService";
+
+ private final ProcessWrapper mProcessWrapper;
+
+ @Inject
+ SystemUISecondaryUserService(ProcessWrapper processWrapper) {
+ mProcessWrapper = processWrapper;
+ }
+
@Override
public void onCreate() {
super.onCreate();
+ if (mProcessWrapper.isSystemUser()) {
+ Log.w(TAG, "SecondaryServices started for System User. Stopping it.");
+ stopSelf();
+ return;
+ }
((SystemUIApplication) getApplication()).startSecondaryUserServicesIfNeeded();
}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
index 76c2282..b26be0c 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
@@ -77,7 +77,7 @@
super.onCreate();
// Start all of SystemUI
- ((SystemUIApplication) getApplication()).startServicesIfNeeded();
+ ((SystemUIApplication) getApplication()).startSystemUserServicesIfNeeded();
// Finish initializing dump logic
mLogBufferFreezer.attach(mBroadcastDispatcher);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
index bf121fb..e57323b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
@@ -26,6 +26,7 @@
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
+import android.text.Layout;
import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;
@@ -162,6 +163,7 @@
mTextView.setPadding(/* left= */ 0, textPadding, /* right= */ 0, textPadding);
mTextView.setTextSize(COMPLEX_UNIT_PX, textSize);
mTextView.setTextColor(textColor);
+ mTextView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL);
final ColorStateList colorAccent = Utils.getColorAccent(getContext());
mUndoButton.setText(res.getString(R.string.accessibility_floating_button_undo));
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt
new file mode 100644
index 0000000..d235c95
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.common.shared.model
+
+import android.annotation.AttrRes
+import android.annotation.ColorInt
+
+/**
+ * Models a color that can be either a specific [Color.Loaded] value or a resolvable theme
+ * [Color.Attribute]
+ */
+sealed interface Color {
+
+ data class Loaded(@ColorInt val color: Int) : Color
+
+ data class Attribute(@AttrRes val attribute: Int) : Color
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
index 12be32c..964eb6f 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
@@ -24,17 +24,12 @@
import androidx.annotation.LayoutRes
import com.android.settingslib.Utils
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.onDensityOrFontScaleChanged
import com.android.systemui.statusbar.policy.onThemeChanged
import com.android.systemui.util.kotlin.emitOnStart
-import com.android.systemui.util.view.bindLatest
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
@@ -91,46 +86,3 @@
.map { layoutInflater.inflate(id, root, attachToRoot) as T }
}
}
-
-/**
- * Perform an inflation right away, then re-inflate whenever the device configuration changes, and
- * call [onInflate] on the resulting view each time. Disposes of the [DisposableHandle] returned by
- * [onInflate] when done.
- *
- * This never completes unless cancelled, it just suspends and waits for updates. It runs on a
- * background thread using [backgroundDispatcher].
- *
- * For parameters [resource], [root] and [attachToRoot], see [LayoutInflater.inflate].
- *
- * An example use-case of this is when a view needs to be re-inflated whenever a configuration
- * change occurs, which would require the ViewBinder to then re-bind the new view. For example, the
- * code in the parent view's binder would look like:
- * ```
- * parentView.repeatWhenAttached {
- * configurationState
- * .reinflateAndBindLatest(
- * R.layout.my_layout,
- * parentView,
- * attachToRoot = false,
- * coroutineScope = lifecycleScope,
- * configurationController.onThemeChanged,
- * ) { view: ChildView ->
- * ChildViewBinder.bind(view, childViewModel)
- * }
- * }
- * ```
- *
- * In turn, the bind method (passed through [onInflate]) uses [repeatWhenAttached], which returns a
- * [DisposableHandle].
- */
-suspend fun <T : View> ConfigurationState.reinflateAndBindLatest(
- @LayoutRes resource: Int,
- root: ViewGroup?,
- attachToRoot: Boolean,
- backgroundDispatcher: CoroutineDispatcher,
- onInflate: (T) -> DisposableHandle?,
-) {
- inflateLayout<T>(resource, root, attachToRoot)
- .flowOn(backgroundDispatcher)
- .bindLatest(onInflate)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index dd186d6..f7bc5cdc 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -26,10 +26,13 @@
import com.android.systemui.ScreenDecorationsModule;
import com.android.systemui.accessibility.SystemActionsModule;
import com.android.systemui.battery.BatterySaverModule;
+import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel;
import com.android.systemui.dock.DockManager;
import com.android.systemui.dock.DockManagerImpl;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.media.dagger.MediaModule;
+import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
+import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.navigationbar.NavigationBarControllerModule;
import com.android.systemui.navigationbar.gestural.GestureModule;
import com.android.systemui.plugins.qs.QSFactory;
@@ -63,6 +66,7 @@
import com.android.systemui.statusbar.policy.SensorPrivacyController;
import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl;
import com.android.systemui.toast.ToastModule;
+import com.android.systemui.unfold.SysUIUnfoldStartableModule;
import com.android.systemui.unfold.UnfoldTransitionModule;
import com.android.systemui.volume.dagger.VolumeModule;
import com.android.systemui.wallpapers.dagger.WallpaperModule;
@@ -92,12 +96,15 @@
AospPolicyModule.class,
BatterySaverModule.class,
CollapsedStatusBarFragmentStartableModule.class,
+ ConnectingDisplayViewModel.StartableModule.class,
GestureModule.class,
HeadsUpModule.class,
KeyboardShortcutsModule.class,
MediaModule.class,
+ MediaMuteAwaitConnectionCli.StartableModule.class,
MultiUserUtilsModule.class,
NavigationBarControllerModule.class,
+ NearbyMediaDevicesManager.StartableModule.class,
PowerModule.class,
QSModule.class,
RearDisplayModule.class,
@@ -108,6 +115,7 @@
ShadeModule.class,
StartCentralSurfacesModule.class,
SceneContainerFrameworkModule.class,
+ SysUIUnfoldStartableModule.class,
UnfoldTransitionModule.Startables.class,
ToastModule.class,
VolumeModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index e7b8773..3b0c281 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -19,25 +19,15 @@
import com.android.systemui.BootCompleteCacheImpl;
import com.android.systemui.CoreStartable;
import com.android.systemui.Dependency;
-import com.android.systemui.Flags;
import com.android.systemui.InitController;
import com.android.systemui.SystemUIAppComponentFactoryBase;
import com.android.systemui.dagger.qualifiers.PerUser;
-import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardSliceProvider;
-import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
-import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.people.PeopleProvider;
import com.android.systemui.statusbar.NotificationInsetsModule;
import com.android.systemui.statusbar.QsFrameTranslateModule;
import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.unfold.FoldStateLogger;
-import com.android.systemui.unfold.FoldStateLoggingProvider;
-import com.android.systemui.unfold.SysUIUnfoldComponent;
-import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
-import com.android.systemui.unfold.dagger.UnfoldBg;
-import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.desktopmode.DesktopMode;
@@ -126,42 +116,6 @@
}
/**
- * Initializes all the SysUI components.
- */
- default void init() {
- // Initialize components that have no direct tie to the dagger dependency graph,
- // but are critical to this component's operation
- getSysUIUnfoldComponent()
- .ifPresent(
- c -> {
- c.getUnfoldLightRevealOverlayAnimation().init();
- c.getUnfoldTransitionWallpaperController().init();
- c.getUnfoldHapticsPlayer();
- c.getNaturalRotationUnfoldProgressProvider().init();
- c.getUnfoldLatencyTracker().init();
- });
- // No init method needed, just needs to be gotten so that it's created.
- getMediaMuteAwaitConnectionCli();
- getNearbyMediaDevicesManager();
- getConnectingDisplayViewModel().init();
- getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init);
- getFoldStateLogger().ifPresent(FoldStateLogger::init);
-
- Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider;
-
- if (Flags.unfoldAnimationBackgroundProgress()) {
- unfoldTransitionProgressProvider = getBgUnfoldTransitionProgressProvider();
- } else {
- unfoldTransitionProgressProvider = getUnfoldTransitionProgressProvider();
- }
- unfoldTransitionProgressProvider
- .ifPresent(
- (progressProvider) ->
- getUnfoldTransitionProgressForwarder()
- .ifPresent(progressProvider::addCallback));
- }
-
- /**
* Provides a BootCompleteCache.
*/
@SysUISingleton
@@ -180,37 +134,6 @@
ContextComponentHelper getContextComponentHelper();
/**
- * Creates a UnfoldTransitionProgressProvider that calculates progress in the background.
- */
- @SysUISingleton
- @UnfoldBg
- Optional<UnfoldTransitionProgressProvider> getBgUnfoldTransitionProgressProvider();
-
- /**
- * Creates a UnfoldTransitionProgressProvider that calculates progress in the main thread.
- */
- @SysUISingleton
- Optional<UnfoldTransitionProgressProvider> getUnfoldTransitionProgressProvider();
-
- /**
- * Creates a UnfoldTransitionProgressForwarder.
- */
- @SysUISingleton
- Optional<UnfoldTransitionProgressForwarder> getUnfoldTransitionProgressForwarder();
-
- /**
- * Creates a FoldStateLoggingProvider.
- */
- @SysUISingleton
- Optional<FoldStateLoggingProvider> getFoldStateLoggingProvider();
-
- /**
- * Creates a FoldStateLogger.
- */
- @SysUISingleton
- Optional<FoldStateLogger> getFoldStateLogger();
-
- /**
* Main dependency providing module.
*/
@SysUISingleton
@@ -227,22 +150,6 @@
InitController getInitController();
/**
- * For devices with a hinge: access objects within this component
- */
- Optional<SysUIUnfoldComponent> getSysUIUnfoldComponent();
-
- /** */
- MediaMuteAwaitConnectionCli getMediaMuteAwaitConnectionCli();
-
- /** */
- NearbyMediaDevicesManager getNearbyMediaDevicesManager();
-
- /**
- * Creates a ConnectingDisplayViewModel
- */
- ConnectingDisplayViewModel getConnectingDisplayViewModel();
-
- /**
* Returns {@link CoreStartable}s that should be started with the application.
*/
Map<Class<?>, Provider<CoreStartable>> getStartables();
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 28fd9a9..1720de8 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -17,6 +17,7 @@
package com.android.systemui.dagger;
import android.app.INotificationManager;
+import android.app.Service;
import android.content.Context;
import android.service.dreams.IDreamManager;
@@ -28,6 +29,7 @@
import com.android.systemui.BootCompleteCache;
import com.android.systemui.BootCompleteCacheImpl;
import com.android.systemui.CameraProtectionModule;
+import com.android.systemui.SystemUISecondaryUserService;
import com.android.systemui.accessibility.AccessibilityModule;
import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule;
import com.android.systemui.appops.dagger.AppOpsModule;
@@ -150,6 +152,8 @@
import dagger.BindsOptionalOf;
import dagger.Module;
import dagger.Provides;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
import java.util.Collections;
import java.util.Optional;
@@ -384,4 +388,9 @@
@Binds
abstract LargeScreenShadeInterpolator largeScreensShadeInterpolator(
LargeScreenShadeInterpolatorImpl impl);
+
+ @Binds
+ @IntoMap
+ @ClassKey(SystemUISecondaryUserService.class)
+ abstract Service bindsSystemUISecondaryUserService(SystemUISecondaryUserService service);
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
index 684627b..2461c26 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
@@ -39,4 +39,6 @@
/** Provide the current status of fingerprint authentication. */
val authenticationStatus: Flow<FingerprintAuthenticationStatus> =
repository.authenticationStatus
+
+ val isLockedOut: Flow<Boolean> = repository.isLockedOut
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
index 98130eb..cf91e14 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
@@ -36,7 +36,6 @@
import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.log.FaceAuthenticationLogger
@@ -78,7 +77,7 @@
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val faceAuthenticationLogger: FaceAuthenticationLogger,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+ private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
private val userRepository: UserRepository,
private val facePropertyRepository: FacePropertyRepository,
private val faceWakeUpTriggersConfig: FaceWakeUpTriggersConfig,
@@ -149,14 +148,24 @@
}
.launchIn(applicationScope)
- deviceEntryFingerprintAuthRepository.isLockedOut
- .onEach {
- if (it) {
+ deviceEntryFingerprintAuthInteractor.isLockedOut
+ .sample(biometricSettingsRepository.isFaceAuthEnrolledAndEnabled, ::Pair)
+ .filter { (_, faceEnabledAndEnrolled) ->
+ // We don't care about this if face auth is not enabled.
+ faceEnabledAndEnrolled
+ }
+ .map { (fpLockedOut, _) -> fpLockedOut }
+ .sample(userRepository.selectedUser, ::Pair)
+ .onEach { (fpLockedOut, currentUser) ->
+ if (fpLockedOut) {
faceAuthenticationLogger.faceLockedOut("Fingerprint locked out")
- // We don't care about this if face auth is not enabled.
if (isFaceAuthEnabledAndEnrolled()) {
repository.setLockedOut(true)
}
+ } else {
+ // Fingerprint is not locked out anymore, revert face lockout state back to
+ // previous value.
+ resetLockedOutState(currentUser.userInfo.id)
}
}
.launchIn(applicationScope)
@@ -169,10 +178,7 @@
val wasSwitching = previous.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS
val isSwitching = curr.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS
if (wasSwitching && !isSwitching) {
- val lockoutMode = facePropertyRepository.getLockoutMode(curr.userInfo.id)
- repository.setLockedOut(
- lockoutMode == LockoutMode.PERMANENT || lockoutMode == LockoutMode.TIMED
- )
+ resetLockedOutState(curr.userInfo.id)
yield()
runFaceAuth(
FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING,
@@ -185,6 +191,13 @@
.launchIn(applicationScope)
}
+ private suspend fun resetLockedOutState(currentUserId: Int) {
+ val lockoutMode = facePropertyRepository.getLockoutMode(currentUserId)
+ repository.setLockedOut(
+ lockoutMode == LockoutMode.PERMANENT || lockoutMode == LockoutMode.TIMED
+ )
+ }
+
override fun onSwipeUpOnBouncer() {
runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
index 10aa703..190062c 100644
--- a/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt
@@ -18,6 +18,7 @@
import android.app.Dialog
import android.content.Context
import com.android.server.policy.feature.flags.Flags
+import com.android.systemui.CoreStartable
import com.android.systemui.biometrics.Utils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -26,6 +27,10 @@
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay
import com.android.systemui.display.ui.view.MirroringConfirmationDialog
import com.android.systemui.statusbar.policy.ConfigurationController
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -47,12 +52,12 @@
@Application private val scope: CoroutineScope,
@Background private val bgDispatcher: CoroutineDispatcher,
private val configurationController: ConfigurationController,
-) {
+) : CoreStartable {
private var dialog: Dialog? = null
/** Starts listening for pending displays. */
- fun init() {
+ override fun start() {
val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay
val concurrentDisplaysInProgessFlow =
if (Flags.enableDualDisplayBlocking()) {
@@ -96,4 +101,12 @@
dialog?.hide()
dialog = null
}
+
+ @Module
+ interface StartableModule {
+ @Binds
+ @IntoMap
+ @ClassKey(ConnectingDisplayViewModel::class)
+ fun bindsConnectingDisplayViewModel(impl: ConnectingDisplayViewModel): CoreStartable
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt
new file mode 100644
index 0000000..304fdd6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.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.haptics.slider
+
+import android.view.View
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.awaitCancellation
+
+object HapticSliderViewBinder {
+ /**
+ * Binds a [SeekableSliderHapticPlugin] to a [View]. The binded view should be a
+ * [android.widget.SeekBar] or a container of a [android.widget.SeekBar]
+ */
+ @JvmStatic
+ fun bind(view: View?, plugin: SeekableSliderHapticPlugin) {
+ view?.repeatWhenAttached {
+ plugin.startInScope(lifecycleScope)
+ try {
+ awaitCancellation()
+ } finally {
+ plugin.stop()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
index 58fb6a9..931a869 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
@@ -20,11 +20,8 @@
import android.view.VelocityTracker
import android.widget.SeekBar
import androidx.annotation.VisibleForTesting
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -43,10 +40,8 @@
constructor(
vibratorHelper: VibratorHelper,
systemClock: SystemClock,
- @Main private val mainDispatcher: CoroutineDispatcher,
- @Application private val applicationScope: CoroutineScope,
sliderHapticFeedbackConfig: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(),
- sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
+ private val sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
) {
private val velocityTracker = VelocityTracker.obtain()
@@ -61,19 +56,15 @@
systemClock,
)
- private val sliderTracker =
- SeekableSliderTracker(
- sliderHapticFeedbackProvider,
- sliderEventProducer,
- mainDispatcher,
- sliderTrackerConfig,
- )
+ private var sliderTracker: SeekableSliderTracker? = null
+
+ private var pluginScope: CoroutineScope? = null
val isTracking: Boolean
- get() = sliderTracker.isTracking
+ get() = sliderTracker?.isTracking == true
- val trackerState: SliderState
- get() = sliderTracker.currentState
+ val trackerState: SliderState?
+ get() = sliderTracker?.currentState
/**
* A waiting [Job] for a timer that estimates the key-up event when a key-down event is
@@ -89,14 +80,20 @@
get() = keyUpJob != null && keyUpJob?.isActive == true
/**
- * Start the plugin.
- *
- * This starts the tracking of slider states, events and triggering of haptic feedback.
+ * Specify the scope for the plugin's operations and start the slider tracker in this scope.
+ * This also involves the key-up timer job.
*/
- fun start() {
- if (!isTracking) {
- sliderTracker.startTracking()
- }
+ fun startInScope(scope: CoroutineScope) {
+ if (sliderTracker != null) stop()
+ sliderTracker =
+ SeekableSliderTracker(
+ sliderHapticFeedbackProvider,
+ sliderEventProducer,
+ scope,
+ sliderTrackerConfig,
+ )
+ pluginScope = scope
+ sliderTracker?.startTracking()
}
/**
@@ -104,7 +101,7 @@
*
* This stops the tracking of slider states, events and triggers of haptic feedback.
*/
- fun stop() = sliderTracker.stopTracking()
+ fun stop() = sliderTracker?.stopTracking()
/** React to a touch event */
fun onTouchEvent(event: MotionEvent?) {
@@ -147,9 +144,9 @@
/**
* An external key was pressed (e.g., a volume key).
*
- * This event is used to estimate the key-up event based on by running a timer as a waiting
- * coroutine in the [keyUpTimerScope]. A key-up event in a slider corresponds to an onArrowUp
- * event. Therefore, [onArrowUp] must be called after the timeout.
+ * This event is used to estimate the key-up event based on a running a timer as a waiting
+ * coroutine in the [pluginScope]. A key-up event in a slider corresponds to an onArrowUp event.
+ * Therefore, [onArrowUp] must be called after the timeout.
*/
fun onKeyDown() {
if (!isTracking) return
@@ -159,7 +156,7 @@
keyUpJob?.cancel()
}
keyUpJob =
- applicationScope.launch {
+ pluginScope?.launch {
delay(KEY_UP_TIMEOUT)
onArrowUp()
}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
index 10098fa..0af3038 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
@@ -17,9 +17,7 @@
package com.android.systemui.haptics.slider
import androidx.annotation.VisibleForTesting
-import com.android.systemui.dagger.qualifiers.Main
import kotlin.math.abs
-import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -31,21 +29,20 @@
*
* The tracker runs a state machine to execute actions on touch-based events typical of a seekable
* slider such as [android.widget.SeekBar]. Coroutines responsible for running the state machine,
- * collecting slider events and maintaining waiting states are run on the main thread via the
- * [com.android.systemui.dagger.qualifiers.Main] coroutine dispatcher.
+ * collecting slider events and maintaining waiting states are run on the provided [CoroutineScope].
*
* @param[sliderStateListener] Listener of the slider state.
* @param[sliderEventProducer] Producer of slider events arising from the slider.
- * @param[mainDispatcher] [CoroutineDispatcher] used to launch coroutines for the collection of
- * slider events and the launch of timer jobs.
+ * @param[trackerScope] [CoroutineScope] used to launch coroutines for the collection of slider
+ * events and the launch of timer jobs.
* @property[config] Configuration parameters of the slider tracker.
*/
class SeekableSliderTracker(
sliderStateListener: SliderStateListener,
sliderEventProducer: SliderEventProducer,
- @Main mainDispatcher: CoroutineDispatcher,
+ trackerScope: CoroutineScope,
private val config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
-) : SliderTracker(CoroutineScope(mainDispatcher), sliderStateListener, sliderEventProducer) {
+) : SliderTracker(trackerScope, sliderStateListener, sliderEventProducer) {
// History of the latest progress collected from slider events
private var latestProgress = 0f
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 4cabd70..2e233d8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -327,7 +327,7 @@
@Override
public void onCreate() {
- ((SystemUIApplication) getApplication()).startServicesIfNeeded();
+ ((SystemUIApplication) getApplication()).startSystemUserServicesIfNeeded();
if (mShellTransitions == null || !Transitions.ENABLE_SHELL_TRANSITIONS) {
RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 8f08efa..39bbf07 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -131,7 +131,7 @@
import com.android.systemui.Dumpable;
import com.android.systemui.EventLogTags;
import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.animation.LaunchAnimator;
+import com.android.systemui.animation.TransitionAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dagger.qualifiers.Main;
@@ -960,7 +960,7 @@
final ActivityLaunchAnimator.Controller mOccludeAnimationController =
new ActivityLaunchAnimator.Controller() {
@Override
- public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {
+ public void onTransitionAnimationStart(boolean isExpandingFullyAbove) {
mOccludeAnimationPlaying = true;
mScrimControllerLazy.get().setOccludeAnimationPlaying(true);
}
@@ -977,7 +977,7 @@
}
@Override
- public void onLaunchAnimationEnd(boolean launchIsFullScreen) {
+ public void onTransitionAnimationEnd(boolean launchIsFullScreen) {
if (launchIsFullScreen) {
mShadeController.get().instantCollapseShade();
}
@@ -994,13 +994,13 @@
@NonNull
@Override
- public ViewGroup getLaunchContainer() {
+ public ViewGroup getTransitionContainer() {
return ((ViewGroup) mKeyguardViewControllerLazy.get()
.getViewRootImpl().getView());
}
@Override
- public void setLaunchContainer(@NonNull ViewGroup launchContainer) {
+ public void setTransitionContainer(@NonNull ViewGroup transitionContainer) {
// No-op, launch container is always the shade.
Log.wtf(TAG, "Someone tried to change the launch container for the "
+ "ActivityLaunchAnimator, which should never happen.");
@@ -1008,9 +1008,9 @@
@NonNull
@Override
- public LaunchAnimator.State createAnimatorState() {
- final int fullWidth = getLaunchContainer().getWidth();
- final int fullHeight = getLaunchContainer().getHeight();
+ public TransitionAnimator.State createAnimatorState() {
+ final int fullWidth = getTransitionContainer().getWidth();
+ final int fullHeight = getTransitionContainer().getHeight();
if (mUpdateMonitor.isSecureCameraLaunchedOverKeyguard()) {
final float initialHeight = fullHeight / 3f;
@@ -1018,7 +1018,7 @@
// Start the animation near the power button, at one-third size, since the
// camera was launched from the power button.
- return new LaunchAnimator.State(
+ return new TransitionAnimator.State(
(int) (mPowerButtonY - initialHeight / 2f) /* top */,
(int) (mPowerButtonY + initialHeight / 2f) /* bottom */,
(int) (fullWidth - initialWidth) /* left */,
@@ -1030,7 +1030,7 @@
// Start the animation in the center of the screen, scaled down to half
// size.
- return new LaunchAnimator.State(
+ return new TransitionAnimator.State(
(int) (fullHeight - initialHeight) / 2,
(int) (initialHeight + (fullHeight - initialHeight) / 2),
(int) (fullWidth - initialWidth) / 2,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
index cc1cf91..7ae70a9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BurnInInteractor.kt
@@ -74,6 +74,7 @@
BurnInModel(translationX, translationY, burnInHelperWrapper.burnInScale())
}
.distinctUntilChanged()
+ .stateIn(scope, SharingStarted.Lazily, BurnInModel())
/**
* Use for max burn-in offsets that are NOT specified in pixels. This flow will recalculate the
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 5606d43..e0b5c0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -98,6 +98,8 @@
val modeOnCanceled =
if (lastStartedStep.from == KeyguardState.LOCKSCREEN) {
TransitionModeOnCanceled.REVERSE
+ } else if (lastStartedStep.from == KeyguardState.GONE) {
+ TransitionModeOnCanceled.RESET
} else {
TransitionModeOnCanceled.LAST_VALUE
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 00b7989..8b278cd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -112,6 +112,38 @@
interpolator: Interpolator = LINEAR,
name: String? = null
): Flow<Float> {
+ return sharedFlowWithState(
+ duration = duration,
+ onStep = onStep,
+ startTime = startTime,
+ onStart = onStart,
+ onCancel = onCancel,
+ onFinish = onFinish,
+ interpolator = interpolator,
+ name = name,
+ )
+ .mapNotNull { stateToValue -> stateToValue.value }
+ }
+
+ /**
+ * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted
+ * in the range of [0, 1]. View animations should begin and end within a subset of this
+ * range. This function maps the [startTime] and [duration] into [0, 1], when this subset is
+ * valid.
+ *
+ * Will return a [StateToValue], which encompasses the calculated value as well as the
+ * transitionState that is associated with it.
+ */
+ fun sharedFlowWithState(
+ duration: Duration,
+ onStep: (Float) -> Float,
+ startTime: Duration = 0.milliseconds,
+ onStart: (() -> Unit)? = null,
+ onCancel: (() -> Float)? = null,
+ onFinish: (() -> Float)? = null,
+ interpolator: Interpolator = LINEAR,
+ name: String? = null
+ ): Flow<StateToValue> {
if (!duration.isPositive()) {
throw IllegalArgumentException("duration must be a positive number: $duration")
}
@@ -164,7 +196,6 @@
.also { logger.logTransitionStep(name, step, it.value) }
}
.distinctUntilChanged()
- .mapNotNull { stateToValue -> stateToValue.value }
}
/**
@@ -174,9 +205,9 @@
return sharedFlow(duration = 1.milliseconds, onStep = { value }, onFinish = { value })
}
}
-
- data class StateToValue(
- val transitionState: TransitionState,
- val value: Float?,
- )
}
+
+data class StateToValue(
+ val transitionState: TransitionState = TransitionState.FINISHED,
+ val value: Float? = 0f,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 789d30f..9e7c70d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -145,9 +145,7 @@
launch {
viewModel.burnInLayerVisibility.collect { visibility ->
childViews[burnInLayerId]?.visibility = visibility
- // Reset alpha only for the icons, as they currently have their
- // own animator
- childViews[aodNotificationIconContainerId]?.alpha = 0f
+ childViews[aodNotificationIconContainerId]?.visibility = visibility
}
}
@@ -313,6 +311,12 @@
}
}
+ if (KeyguardShadeMigrationNssl.isEnabled) {
+ burnInParams.update { current ->
+ current.copy(translationY = { childViews[burnInLayerId]?.translationY })
+ }
+ }
+
onLayoutChangeListener = OnLayoutChange(viewModel, burnInParams)
view.addOnLayoutChangeListener(onLayoutChangeListener)
@@ -435,11 +439,17 @@
}
when {
!isVisible.isAnimating -> {
- alpha = 1f
if (!KeyguardShadeMigrationNssl.isEnabled) {
translationY = 0f
}
- visibility = if (isVisible.value) View.VISIBLE else View.INVISIBLE
+ visibility =
+ if (isVisible.value) {
+ alpha = 1f
+ View.VISIBLE
+ } else {
+ alpha = 0f
+ View.INVISIBLE
+ }
}
newAodTransition() -> {
animateInIconTranslation()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
index 9cf3c95..d4ea728 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt
@@ -28,6 +28,7 @@
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
/** Models UI state for the alpha of the AOD (always-on display). */
@SysUISingleton
@@ -42,13 +43,15 @@
/** The alpha level for the entire lockscreen while in AOD. */
val alpha: Flow<Float> =
combine(
- keyguardTransitionInteractor.currentKeyguardState,
+ keyguardTransitionInteractor.transitionValue(KeyguardState.GONE).onStart {
+ emit(0f)
+ },
merge(
keyguardInteractor.keyguardAlpha,
occludedToLockscreenTransitionViewModel.lockscreenAlpha,
)
- ) { currentKeyguardState, alpha ->
- if (currentKeyguardState == KeyguardState.GONE) {
+ ) { transitionToGone, alpha ->
+ if (transitionToGone == 1f) {
// Ensures content is not visible when in GONE state
0f
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
index 828e033..8110de2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
@@ -31,6 +31,10 @@
import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.ui.StateToValue
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.res.R
import javax.inject.Inject
@@ -58,6 +62,7 @@
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
+ private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
private val keyguardClockViewModel: KeyguardClockViewModel,
) {
@@ -83,21 +88,22 @@
burnIn(params).map { it.translationY.toFloat() }.onStart { emit(0f) },
goneToAodTransitionViewModel
.enterFromTopTranslationY(enterFromTopAmount)
- .onStart { emit(0f) },
+ .onStart { emit(StateToValue()) },
occludedToLockscreenTransitionViewModel.lockscreenTranslationY.onStart {
emit(0f)
},
- ) {
- keyguardTransitionY,
- burnInTranslationY,
- goneToAodTransitionTranslationY,
- occludedToLockscreenTransitionTranslationY ->
-
- // All values need to be combined for a smooth translation
- keyguardTransitionY +
- burnInTranslationY +
- goneToAodTransitionTranslationY +
- occludedToLockscreenTransitionTranslationY
+ aodToLockscreenTransitionViewModel.translationY(params.translationY).onStart {
+ emit(StateToValue())
+ },
+ ) { keyguardTranslationY, burnInY, goneToAod, occludedToLockscreen, aodToLockscreen
+ ->
+ if (isInTransition(aodToLockscreen.transitionState)) {
+ aodToLockscreen.value ?: 0f
+ } else if (isInTransition(goneToAod.transitionState)) {
+ (goneToAod.value ?: 0f) + burnInY
+ } else {
+ burnInY + occludedToLockscreen + keyguardTranslationY
+ }
}
}
.distinctUntilChanged()
@@ -115,6 +121,10 @@
}
}
+ private fun isInTransition(state: TransitionState): Boolean {
+ return state == STARTED || state == RUNNING
+ }
+
private fun burnIn(
params: BurnInParameters,
): Flow<BurnInModel> {
@@ -185,6 +195,8 @@
val topInset: Int = 0,
/** Status view top, without translation added in */
val statusViewTop: Int = 0,
+ /** The current y translation of the view */
+ val translationY: () -> Float? = { null }
)
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
index 266fd02..6d1d3cb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt
@@ -16,11 +16,14 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.util.MathUtils
+import com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.StateToValue
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -48,6 +51,22 @@
to = KeyguardState.LOCKSCREEN,
)
+ /**
+ * Begin the transition from wherever the y-translation value is currently. This helps ensure a
+ * smooth transition if a transition in canceled.
+ */
+ fun translationY(currentTranslationY: () -> Float?): Flow<StateToValue> {
+ var startValue = 0f
+ return transitionAnimation.sharedFlowWithState(
+ duration = 500.milliseconds,
+ onStart = {
+ startValue = currentTranslationY() ?: 0f
+ startValue
+ },
+ onStep = { MathUtils.lerp(startValue, 0f, FAST_OUT_SLOW_IN.getInterpolation(it)) },
+ )
+ }
+
/** Ensure alpha is set to be visible */
val lockscreenAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
index f5e6135..85885b0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModel.kt
@@ -22,6 +22,7 @@
import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_AOD_DURATION
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.StateToValue
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -48,8 +49,8 @@
)
/** y-translation from the top of the screen for AOD */
- fun enterFromTopTranslationY(translatePx: Int): Flow<Float> {
- return transitionAnimation.sharedFlow(
+ fun enterFromTopTranslationY(translatePx: Int): Flow<StateToValue> {
+ return transitionAnimation.sharedFlowWithState(
startTime = 600.milliseconds,
duration = 500.milliseconds,
onStart = { translatePx },
@@ -63,8 +64,8 @@
/** alpha animation upon entering AOD */
val enterFromTopAnimationAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
- startTime = 600.milliseconds,
- duration = 500.milliseconds,
+ startTime = 700.milliseconds,
+ duration = 400.milliseconds,
onStart = { 0f },
onStep = { it },
onFinish = { 1f },
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index f8a12bd..ec13228 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -30,6 +30,8 @@
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
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.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
@@ -48,6 +50,7 @@
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
@@ -78,6 +81,12 @@
val goneToAodTransition = keyguardTransitionInteractor.transition(from = GONE, to = AOD)
+ private val goneToAodTransitionRunning: Flow<Boolean> =
+ goneToAodTransition
+ .map { it.transitionState == STARTED || it.transitionState == RUNNING }
+ .onStart { emit(false) }
+ .distinctUntilChanged()
+
/** Last point that the root view was tapped */
val lastRootViewTapPosition: Flow<Point?> = keyguardInteractor.lastRootViewTapPosition
@@ -138,6 +147,7 @@
/** Is the notification icon container visible? */
val isNotifIconContainerVisible: Flow<AnimatedValue<Boolean>> =
combine(
+ goneToAodTransitionRunning,
keyguardTransitionInteractor.finishedKeyguardState.map {
KeyguardState.lockscreenVisibleInState(it)
},
@@ -145,6 +155,7 @@
areNotifsFullyHiddenAnimated(),
isPulseExpandingAnimated(),
) {
+ goneToAodTransitionRunning: Boolean,
onKeyguard: Boolean,
isBypassEnabled: Boolean,
notifsFullyHidden: AnimatedValue<Boolean>,
@@ -154,7 +165,9 @@
// Hide the AOD icons if we're not in the KEYGUARD state unless the screen off
// animation is playing, in which case we want them to be visible if we're
// animating in the AOD UI and will be switching to KEYGUARD shortly.
- !onKeyguard && !screenOffAnimationController.shouldShowAodIconsWhenShade() ->
+ goneToAodTransitionRunning ||
+ (!onKeyguard &&
+ !screenOffAnimationController.shouldShowAodIconsWhenShade()) ->
AnimatedValue.NotAnimating(false)
else ->
zip(notifsFullyHidden, pulseExpanding) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index 7a48836..3ab0420 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -16,9 +16,6 @@
package com.android.systemui.media;
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
@@ -37,8 +34,6 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
-import android.os.VibrationEffect;
-import android.os.vibrator.Flags;
import android.provider.MediaStore;
import android.util.Log;
@@ -58,7 +53,7 @@
@SysUISingleton
public class RingtonePlayer implements CoreStartable {
private static final String TAG = "RingtonePlayer";
- private static final boolean LOGD = true;
+ private static final boolean LOGD = false;
private final Context mContext;
// TODO: support Uri switching under same IBinder
@@ -91,11 +86,20 @@
*/
private class Client implements IBinder.DeathRecipient {
private final IBinder mToken;
- private Ringtone mRingtone;
+ private final Ringtone mRingtone;
- Client(@NonNull IBinder token, @NonNull Ringtone ringtone) {
- mToken = requireNonNull(token);
- mRingtone = requireNonNull(ringtone);
+ public Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa) {
+ this(token, uri, user, aa, null);
+ }
+
+ Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa,
+ @Nullable VolumeShaper.Configuration volumeShaperConfig) {
+ mToken = token;
+
+ mRingtone = new Ringtone(getContextForUser(user), false);
+ mRingtone.setAudioAttributesField(aa);
+ mRingtone.setUri(uri, volumeShaperConfig);
+ mRingtone.createLocalMediaPlayer();
}
@Override
@@ -112,48 +116,24 @@
@Override
public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping)
throws RemoteException {
- if (Flags.hapticsCustomizationRingtoneV2Enabled()) {
- playRemoteRingtone(token, uri, aa, true, Ringtone.MEDIA_SOUND,
- null, volume, looping, /* hapticGenerator= */ false,
- null);
- } else {
- playWithVolumeShaping(token, uri, aa, volume, looping, null);
- }
+ playWithVolumeShaping(token, uri, aa, volume, looping, null);
}
-
@Override
- public void playWithVolumeShaping(
- IBinder token, Uri uri, AudioAttributes aa, float volume,
+ public void playWithVolumeShaping(IBinder token, Uri uri, AudioAttributes aa, float volume,
boolean looping, @Nullable VolumeShaper.Configuration volumeShaperConfig)
throws RemoteException {
if (LOGD) {
- Log.d(TAG, "playWithVolumeShaping(token=" + token + ", uri=" + uri + ", uid="
+ Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid="
+ Binder.getCallingUid() + ")");
}
Client client;
synchronized (mClients) {
client = mClients.get(token);
- }
- // Don't hold the lock while constructing the ringtone, since it can be slow. The caller
- // shouldn't call play on the same ringtone from 2 threads, so this shouldn't race and
- // waste the build.
- if (client == null) {
- final UserHandle user = Binder.getCallingUserHandle();
- Ringtone ringtone = Ringtone.createV1WithCustomAudioAttributes(
- getContextForUser(user), aa, uri, volumeShaperConfig,
- /* allowRemote= */ false);
- synchronized (mClients) {
- client = mClients.get(token);
- if (client == null) {
- client = new Client(token, ringtone);
- token.linkToDeath(client, 0);
- mClients.put(token, client);
- ringtone = null; // "owned" by the client now.
- }
- }
- // Clean up ringtone if it was abandoned (a client already existed).
- if (ringtone != null) {
- ringtone.stop();
+ if (client == null) {
+ final UserHandle user = Binder.getCallingUserHandle();
+ client = new Client(token, uri, user, aa, volumeShaperConfig);
+ token.linkToDeath(client, 0);
+ mClients.put(token, client);
}
}
client.mRingtone.setLooping(looping);
@@ -162,54 +142,6 @@
}
@Override
- public void playRemoteRingtone(IBinder token, Uri uri, AudioAttributes aa,
- boolean useExactAudioAttributes,
- @Ringtone.RingtoneMedia int enabledMedia, @Nullable VibrationEffect vibrationEffect,
- float volume,
- boolean looping, boolean isHapticGeneratorEnabled,
- @Nullable VolumeShaper.Configuration volumeShaperConfig)
- throws RemoteException {
- if (LOGD) {
- Log.d(TAG, "playRemoteRingtone(token=" + token + ", uri=" + uri + ", uid="
- + Binder.getCallingUid() + ")");
- }
-
- // Don't hold the lock while constructing the ringtone, since it can be slow. The caller
- // shouldn't call play on the same ringtone from 2 threads, so this shouldn't race and
- // waste the build.
- Client client;
- synchronized (mClients) {
- client = mClients.get(token);
- }
- if (client == null) {
- final UserHandle user = Binder.getCallingUserHandle();
- Ringtone ringtone = new Ringtone.Builder(getContextForUser(user), enabledMedia, aa)
- .setLocalOnly()
- .setUri(uri)
- .setLooping(looping)
- .setInitialSoundVolume(volume)
- .setUseExactAudioAttributes(useExactAudioAttributes)
- .setEnableHapticGenerator(isHapticGeneratorEnabled)
- .setVibrationEffect(vibrationEffect)
- .setVolumeShaperConfig(volumeShaperConfig)
- .build();
- if (ringtone == null) {
- return;
- }
- synchronized (mClients) {
- client = mClients.get(token);
- if (client == null) {
- client = new Client(token, ringtone);
- token.linkToDeath(client, 0);
- mClients.put(token, client);
- }
- }
- }
- // Ensure the client is initialized outside the all-clients lock, as it can be slow.
- client.mRingtone.play();
- }
-
- @Override
public void stop(IBinder token) {
if (LOGD) Log.d(TAG, "stop(token=" + token + ")");
Client client;
@@ -235,10 +167,10 @@
return false;
}
}
+
@Override
public void setPlaybackProperties(IBinder token, float volume, boolean looping,
- boolean hapticGeneratorEnabled) {
- // RingtoneV1-exclusive path.
+ boolean hapticGeneratorEnabled) {
Client client;
synchronized (mClients) {
client = mClients.get(token);
@@ -252,39 +184,6 @@
}
@Override
- public void setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled) {
- Client client;
- synchronized (mClients) {
- client = mClients.get(token);
- }
- if (client != null) {
- client.mRingtone.setHapticGeneratorEnabled(hapticGeneratorEnabled);
- }
- }
-
- @Override
- public void setLooping(IBinder token, boolean looping) {
- Client client;
- synchronized (mClients) {
- client = mClients.get(token);
- }
- if (client != null) {
- client.mRingtone.setLooping(looping);
- }
- }
-
- @Override
- public void setVolume(IBinder token, float volume) {
- Client client;
- synchronized (mClients) {
- client = mClients.get(token);
- }
- if (client != null) {
- client.mRingtone.setVolume(volume);
- }
- }
-
- @Override
public void playAsync(Uri uri, UserHandle user, boolean looping, AudioAttributes aa,
float volume) {
if (LOGD) Log.d(TAG, "playAsync(uri=" + uri + ", user=" + user + ")");
diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt
index 2ae3a63..e475647 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionCli.kt
@@ -16,13 +16,18 @@
package com.android.systemui.media.muteawait
-import android.content.Context
+import android.annotation.SuppressLint
import android.media.AudioAttributes.USAGE_MEDIA
import android.media.AudioDeviceAttributes
import android.media.AudioManager
+import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
import java.io.PrintWriter
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -30,14 +35,15 @@
/** A command line interface to manually test [MediaMuteAwaitConnectionManager]. */
@SysUISingleton
class MediaMuteAwaitConnectionCli @Inject constructor(
- commandRegistry: CommandRegistry,
- private val context: Context
-) {
- init {
+ private val commandRegistry: CommandRegistry,
+ private val audioManager: AudioManager,
+) : CoreStartable {
+ override fun start() {
commandRegistry.registerCommand(MEDIA_MUTE_AWAIT_COMMAND) { MuteAwaitCommand() }
}
inner class MuteAwaitCommand : Command {
+ @SuppressLint("MissingPermission")
override fun execute(pw: PrintWriter, args: List<String>) {
val device = AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT,
@@ -49,8 +55,6 @@
)
val startOrCancel = args[2]
- val audioManager: AudioManager =
- context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
when (startOrCancel) {
START ->
audioManager.muteAwaitConnection(
@@ -65,6 +69,14 @@
"[type] [name] [$START|$CANCEL]")
}
}
+
+ @Module
+ interface StartableModule {
+ @Binds
+ @IntoMap
+ @ClassKey(MediaMuteAwaitConnectionCli::class)
+ fun bindsMediaMuteAwaitConnectionCli(impl: MediaMuteAwaitConnectionCli): CoreStartable
+ }
}
private const val MEDIA_MUTE_AWAIT_COMMAND = "media-mute-await"
diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt
index 64b772b..0dc10f6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt
@@ -18,9 +18,14 @@
import android.media.INearbyMediaDevicesProvider
import android.media.INearbyMediaDevicesUpdateCallback
-import com.android.systemui.dagger.SysUISingleton
import android.os.IBinder
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.CommandQueue
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
import javax.inject.Inject
/**
@@ -30,9 +35,9 @@
*/
@SysUISingleton
class NearbyMediaDevicesManager @Inject constructor(
- commandQueue: CommandQueue,
+ private val commandQueue: CommandQueue,
private val logger: NearbyMediaDevicesLogger
-) {
+) : CoreStartable {
private var providers: MutableList<INearbyMediaDevicesProvider> = mutableListOf()
private var activeCallbacks: MutableList<INearbyMediaDevicesUpdateCallback> = mutableListOf()
@@ -69,7 +74,7 @@
}
}
- init {
+ override fun start() {
commandQueue.addCallback(commandQueueCallbacks)
}
@@ -108,4 +113,12 @@
}
}
}
+
+ @Module
+ interface StartableModule {
+ @Binds
+ @IntoMap
+ @ClassKey(NearbyMediaDevicesManager::class)
+ fun bindsNearbyMediaDevicesManager(impl: NearbyMediaDevicesManager): CoreStartable
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index 0167287..aa03e6e 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -395,7 +395,7 @@
}
if (displayId == mDisplayId) {
mLightBarController.onNavigationBarAppearanceChanged(appearance, nbModeChanged,
- BarTransitions.MODE_TRANSPARENT, navbarColorManagedByIme);
+ mTransitionMode, navbarColorManagedByIme);
}
if (mBehavior != behavior) {
mBehavior = behavior;
diff --git a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
index 2751072..3671dd4 100644
--- a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
@@ -16,6 +16,9 @@
package com.android.systemui.process;
+import android.os.Process;
+import android.os.UserHandle;
+
import javax.inject.Inject;
/**
@@ -30,6 +33,15 @@
* Returns {@code true} if System User is running the current process.
*/
public boolean isSystemUser() {
- return android.os.Process.myUserHandle().isSystem();
+ return myUserHandle().isSystem();
+ }
+
+ /**
+ * Returns {@link UserHandle} as returned statically by {@link Process#myUserHandle()}.
+ *
+ * Please strongly consider using {@link com.android.systemui.settings.UserTracker} instead.
+ */
+ public UserHandle myUserHandle() {
+ return Process.myUserHandle();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index a2dfc01..c0d9644 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -157,6 +157,12 @@
super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
parentHeightMeasureSpec, heightUsed);
}
+ } else {
+ // Don't measure the customizer with all the children, it will be measured separately
+ if (child != mQSCustomizer) {
+ super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
+ parentHeightMeasureSpec, heightUsed);
+ }
}
}
@@ -248,6 +254,25 @@
+ mHeader.getHeight();
}
+ // These next two methods are used with Scene container to determine the size of QQS and QS .
+
+ /**
+ * Returns the size of the QQS container, regardless of the measured size of this view.
+ * @return size in pixels of QQS
+ */
+ public int getQqsHeight() {
+ return mHeader.getHeight();
+ }
+
+ /**
+ * Returns the size of QS (or the QSCustomizer), regardless of the measured size of this view
+ * @return size in pixels of QS (or QSCustomizer)
+ */
+ public int getQsHeight() {
+ return mQSCustomizer.isCustomizing() ? mQSCustomizer.getMeasuredHeight()
+ : mQSPanel.getMeasuredHeight();
+ }
+
public void setExpansion(float expansion) {
mQsExpansion = expansion;
if (mQSPanelContainer != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
index 290821e..4d55714 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -984,6 +984,14 @@
return mListeningAndVisibilityLifecycleOwner;
}
+ public int getQQSHeight() {
+ return mContainer.getQqsHeight();
+ }
+
+ public int getQSHeight() {
+ return mContainer.getQsHeight();
+ }
+
@NeverCompile
@Override
public void dump(PrintWriter pw, String[] args) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index 0d43396..3d12eed 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -28,6 +28,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.qs.QSContainerImpl
import com.android.systemui.qs.QSImpl
import com.android.systemui.qs.dagger.QSSceneComponent
import com.android.systemui.res.R
@@ -68,6 +69,15 @@
/** Set the current state for QS. [state]. */
fun setState(state: State)
+ /** The current height of QQS in the current [qsView], or 0 if there's no view. */
+ val qqsHeight: Int
+
+ /**
+ * The current height of QS in the current [qsView], or 0 if there's no view. If customizing, it
+ * will return the height allocated to the customizer.
+ */
+ val qsHeight: Int
+
sealed class State(
val isVisible: Boolean,
val expansion: Float,
@@ -121,6 +131,11 @@
val qsImpl = _qsImpl.asStateFlow()
override val qsView: Flow<View> = _qsImpl.map { it?.view }.filterNotNull()
+ override val qqsHeight: Int
+ get() = qsImpl.value?.qqsHeight ?: 0
+ override val qsHeight: Int
+ get() = qsImpl.value?.qsHeight ?: 0
+
// Same config changes as in FragmentHostManager
private val interestingChanges =
InterestingConfigChanges(
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index fcbe9a6..356eb85 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -45,8 +45,8 @@
sceneKeys =
listOf(
SceneKey.Gone,
- SceneKey.Shade,
SceneKey.QuickSettings,
+ SceneKey.Shade,
),
initialSceneKey = SceneKey.Gone,
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 0f3acaf..c7d3a4a 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -69,8 +69,8 @@
SceneKey.Communal,
SceneKey.Lockscreen,
SceneKey.Bouncer,
- SceneKey.Shade,
SceneKey.QuickSettings,
+ SceneKey.Shade,
),
initialSceneKey = SceneKey.Lockscreen,
)
diff --git a/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt
index 76d1d3d..6199a83 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/SecureSettingsRepositoryModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index e9af295..861a2ed 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -31,8 +31,8 @@
import com.android.settingslib.RestrictedLockUtils;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.classifier.Classifier;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.haptics.slider.SeekableSliderEventProducer;
+import com.android.systemui.haptics.slider.HapticSliderViewBinder;
+import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.VibratorHelper;
@@ -42,8 +42,6 @@
import javax.inject.Inject;
-import kotlinx.coroutines.CoroutineDispatcher;
-
/**
* {@code ViewController} for a {@code BrightnessSliderView}
*
@@ -63,23 +61,16 @@
private final FalsingManager mFalsingManager;
private final UiEventLogger mUiEventLogger;
- private final BrightnessSliderHapticPlugin mBrightnessSliderHapticPlugin;
+ private final SeekableSliderHapticPlugin mBrightnessSliderHapticPlugin;
private final Gefingerpoken mOnInterceptListener = new Gefingerpoken() {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
+ mBrightnessSliderHapticPlugin.onTouchEvent(ev);
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mFalsingManager.isFalseTouch(Classifier.BRIGHTNESS_SLIDER);
- if (mBrightnessSliderHapticPlugin.getVelocityTracker() != null) {
- mBrightnessSliderHapticPlugin.getVelocityTracker().clear();
- }
- } else if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
- if (mBrightnessSliderHapticPlugin.getVelocityTracker() != null) {
- mBrightnessSliderHapticPlugin.getVelocityTracker().addMovement(ev);
- }
}
-
return false;
}
@@ -93,7 +84,7 @@
BrightnessSliderView brightnessSliderView,
FalsingManager falsingManager,
UiEventLogger uiEventLogger,
- BrightnessSliderHapticPlugin brightnessSliderHapticPlugin) {
+ SeekableSliderHapticPlugin brightnessSliderHapticPlugin) {
super(brightnessSliderView);
mFalsingManager = falsingManager;
mUiEventLogger = uiEventLogger;
@@ -112,7 +103,6 @@
protected void onViewAttached() {
mView.setOnSeekBarChangeListener(mSeekListener);
mView.setOnInterceptListener(mOnInterceptListener);
- mBrightnessSliderHapticPlugin.start();
}
@Override
@@ -120,7 +110,6 @@
mView.setOnSeekBarChangeListener(null);
mView.setOnDispatchTouchEventListener(null);
mView.setOnInterceptListener(null);
- mBrightnessSliderHapticPlugin.stop();
}
@Override
@@ -225,10 +214,8 @@
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (mListener != null) {
mListener.onChanged(mTracking, progress, false);
- SeekableSliderEventProducer eventProducer =
- mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
- if (eventProducer != null && fromUser) {
- eventProducer.onProgressChanged(seekBar, progress, fromUser);
+ if (fromUser) {
+ mBrightnessSliderHapticPlugin.onProgressChanged(seekBar, progress, fromUser);
}
}
}
@@ -239,11 +226,7 @@
mUiEventLogger.log(BrightnessSliderEvent.BRIGHTNESS_SLIDER_STARTED_TRACKING_TOUCH);
if (mListener != null) {
mListener.onChanged(mTracking, getValue(), false);
- SeekableSliderEventProducer eventProducer =
- mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
- if (eventProducer != null) {
- eventProducer.onStartTrackingTouch(seekBar);
- }
+ mBrightnessSliderHapticPlugin.onStartTrackingTouch(seekBar);
}
if (mMirrorController != null) {
@@ -258,11 +241,7 @@
mUiEventLogger.log(BrightnessSliderEvent.BRIGHTNESS_SLIDER_STOPPED_TRACKING_TOUCH);
if (mListener != null) {
mListener.onChanged(mTracking, getValue(), true);
- SeekableSliderEventProducer eventProducer =
- mBrightnessSliderHapticPlugin.getSeekableSliderEventProducer();
- if (eventProducer != null) {
- eventProducer.onStopTrackingTouch(seekBar);
- }
+ mBrightnessSliderHapticPlugin.onStopTrackingTouch(seekBar);
}
if (mMirrorController != null) {
@@ -280,21 +259,18 @@
private final UiEventLogger mUiEventLogger;
private final VibratorHelper mVibratorHelper;
private final SystemClock mSystemClock;
- private final CoroutineDispatcher mMainDispatcher;
@Inject
public Factory(
FalsingManager falsingManager,
UiEventLogger uiEventLogger,
VibratorHelper vibratorHelper,
- SystemClock clock,
- @Main CoroutineDispatcher mainDispatcher
+ SystemClock clock
) {
mFalsingManager = falsingManager;
mUiEventLogger = uiEventLogger;
mVibratorHelper = vibratorHelper;
mSystemClock = clock;
- mMainDispatcher = mainDispatcher;
}
/**
@@ -310,15 +286,11 @@
int layout = getLayout();
BrightnessSliderView root = (BrightnessSliderView) LayoutInflater.from(context)
.inflate(layout, viewRoot, false);
- BrightnessSliderHapticPlugin plugin;
- if (hapticBrightnessSlider()) {
- plugin = new BrightnessSliderHapticPluginImpl(
+ SeekableSliderHapticPlugin plugin = new SeekableSliderHapticPlugin(
mVibratorHelper,
- mSystemClock,
- mMainDispatcher
- );
- } else {
- plugin = new BrightnessSliderHapticPlugin() {};
+ mSystemClock);
+ if (hapticBrightnessSlider()) {
+ HapticSliderViewBinder.bind(viewRoot, plugin);
}
return new BrightnessSliderController(root, mFalsingManager, mUiEventLogger, plugin);
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPlugin.kt
deleted file mode 100644
index f775114..0000000
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPlugin.kt
+++ /dev/null
@@ -1,46 +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.settings.brightness
-
-import android.view.VelocityTracker
-import com.android.systemui.haptics.slider.SeekableSliderEventProducer
-
-/** Plugin component for the System UI brightness slider to incorporate dynamic haptics */
-interface BrightnessSliderHapticPlugin {
-
- /** Finger velocity tracker */
- val velocityTracker: VelocityTracker?
- get() = null
-
- /** Producer of slider events from the underlying [android.widget.SeekBar] */
- val seekableSliderEventProducer: SeekableSliderEventProducer?
- get() = null
-
- /**
- * Start the plugin.
- *
- * This starts the tracking of slider states, events and triggering of haptic feedback.
- */
- fun start() {}
-
- /**
- * Stop the plugin
- *
- * This stops the tracking of slider states, events and triggers of haptic feedback.
- */
- fun stop() {}
-}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImpl.kt
deleted file mode 100644
index 32561f0..0000000
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImpl.kt
+++ /dev/null
@@ -1,69 +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.settings.brightness
-
-import android.view.VelocityTracker
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.haptics.slider.SeekableSliderEventProducer
-import com.android.systemui.haptics.slider.SeekableSliderTracker
-import com.android.systemui.haptics.slider.SliderHapticFeedbackProvider
-import com.android.systemui.haptics.slider.SliderTracker
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.CoroutineDispatcher
-
-/**
- * Implementation of the [BrightnessSliderHapticPlugin].
- *
- * For the specifics of the brightness slider in System UI, a [SeekableSliderEventProducer] is used
- * as the producer of slider events, a [SliderHapticFeedbackProvider] is used as the listener of
- * slider states to play haptic feedback depending on the state, and a [SeekableSliderTracker] is
- * used as the state machine handler that tracks and manipulates the slider state.
- */
-class BrightnessSliderHapticPluginImpl
-@JvmOverloads
-constructor(
- vibratorHelper: VibratorHelper,
- systemClock: SystemClock,
- @Main mainDispatcher: CoroutineDispatcher,
- override val velocityTracker: VelocityTracker = VelocityTracker.obtain(),
- override val seekableSliderEventProducer: SeekableSliderEventProducer =
- SeekableSliderEventProducer(),
-) : BrightnessSliderHapticPlugin {
-
- private val sliderHapticFeedbackProvider: SliderHapticFeedbackProvider =
- SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, clock = systemClock)
- private val sliderTracker: SliderTracker =
- SeekableSliderTracker(
- sliderHapticFeedbackProvider,
- seekableSliderEventProducer,
- mainDispatcher,
- )
-
- val isTracking: Boolean
- get() = sliderTracker.isTracking
-
- override fun start() {
- if (!sliderTracker.isTracking) {
- sliderTracker.startTracking()
- }
- }
-
- override fun stop() {
- sliderTracker.stopTracking()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 133fa12..73d229e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -113,7 +113,7 @@
import com.android.systemui.Dumpable;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.animation.LaunchAnimator;
+import com.android.systemui.animation.TransitionAnimator;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
@@ -1348,7 +1348,7 @@
}
updateClockAppearance();
mQsController.updateQsState();
- if (!KeyguardShadeMigrationNssl.isEnabled()) {
+ if (!KeyguardShadeMigrationNssl.isEnabled() && !FooterViewRefactor.isEnabled()) {
mNotificationStackScrollLayoutController.updateFooter();
}
}
@@ -3231,7 +3231,7 @@
@Override
public void applyLaunchAnimationProgress(float linearProgress) {
- boolean hideIcons = LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS,
+ boolean hideIcons = TransitionAnimator.getProgress(ActivityLaunchAnimator.TIMINGS,
linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
if (hideIcons != mHideIconsDuringLaunchAnimation) {
mHideIconsDuringLaunchAnimation = hideIcons;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 6e85074..ac510fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -156,6 +156,54 @@
}
}
+data class LinearSideLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
+
+ override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
+ scrim.interpolatedRevealAmount = amount
+ scrim.startColorAlpha =
+ getPercentPastThreshold(1 - amount, threshold = 1 - START_COLOR_REVEAL_PERCENTAGE)
+ scrim.revealGradientEndColorAlpha =
+ 1f -
+ getPercentPastThreshold(
+ amount,
+ threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE
+ )
+
+ val gradientBoundsAmount = lerp(GRADIENT_START_BOUNDS_PERCENTAGE, 1f, amount)
+ if (isVertical) {
+ scrim.setRevealGradientBounds(
+ left = -(scrim.viewWidth) * gradientBoundsAmount,
+ top = -(scrim.viewHeight) * gradientBoundsAmount,
+ right = (scrim.viewWidth) * gradientBoundsAmount,
+ bottom = (scrim.viewHeight) + (scrim.viewHeight) * gradientBoundsAmount
+ )
+ } else {
+ scrim.setRevealGradientBounds(
+ left = -(scrim.viewWidth) * gradientBoundsAmount,
+ top = -(scrim.viewHeight) * gradientBoundsAmount,
+ right = (scrim.viewWidth) + (scrim.viewWidth) * gradientBoundsAmount,
+ bottom = (scrim.viewHeight) * gradientBoundsAmount
+ )
+ }
+ }
+
+ private companion object {
+ // From which percentage we should start the gradient reveal width
+ // E.g. if 0 - starts with 0px width, 0.6f - starts with 60% width
+ private const val GRADIENT_START_BOUNDS_PERCENTAGE: Float = 1f
+
+ // When to start changing alpha color of the gradient scrim
+ // E.g. if 0.6f - starts fading the gradient away at 60% and becomes completely
+ // transparent at 100%
+ private const val REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE: Float = 1f
+
+ // When to finish displaying start color fill that reveals the content
+ // E.g. if 0.6f - the content won't be visible at 0% and it will gradually
+ // reduce the alpha until 60% (at this point the color fill is invisible)
+ private const val START_COLOR_REVEAL_PERCENTAGE: Float = 1f
+ }
+}
+
data class CircleReveal(
/** X-value of the circle center of the reveal. */
val centerX: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt
index 785e65d..6f4a7cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/LaunchAnimationParameters.kt
@@ -4,7 +4,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.app.animation.Interpolators
-import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.animation.TransitionAnimator
import kotlin.math.min
/** Parameters for the notifications launch expanding animations. */
@@ -16,7 +16,7 @@
topCornerRadius: Float = 0f,
bottomCornerRadius: Float = 0f
-) : LaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
+) : TransitionAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
@VisibleForTesting
constructor() : this(
top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f
@@ -58,7 +58,7 @@
}
fun getProgress(delay: Long, duration: Long): Float {
- return LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, delay,
+ return TransitionAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, delay,
duration)
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
index 5983fc1..8fc9619 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
@@ -20,7 +20,7 @@
import android.view.ViewGroup
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.animation.TransitionAnimator
import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
@@ -75,13 +75,13 @@
private val notificationEntry = notification.entry
private val notificationKey = notificationEntry.sbn.key
- override var launchContainer: ViewGroup
+ override var transitionContainer: ViewGroup
get() = notification.rootView as ViewGroup
set(ignored) {
// Do nothing. Notifications are always animated inside their rootView.
}
- override fun createAnimatorState(): LaunchAnimator.State {
+ override fun createAnimatorState(): TransitionAnimator.State {
// If the notification panel is collapsed, the clip may be larger than the height.
val height = max(0, notification.actualHeight - notification.clipBottomAmount)
val location = notification.locationOnScreen
@@ -186,14 +186,14 @@
onFinishAnimationCallback?.run()
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
notification.isExpandAnimationRunning = true
notificationListContainer.setExpandingNotification(notification)
jankMonitor.begin(notification, InteractionJankMonitor.CUJ_NOTIFICATION_APP_START)
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
if (ActivityLaunchAnimator.DEBUG_LAUNCH_ANIMATION) {
Log.d(TAG, "onLaunchAnimationEnd()")
}
@@ -213,8 +213,8 @@
notificationListContainer.applyLaunchAnimationParams(params)
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 6e2beb4..8b0b973 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -56,8 +56,8 @@
if (FooterViewRefactor.isEnabled) {
activeNotificationsInteractor.setNotifStats(notifStats)
}
- // TODO(b/293167744): This shouldn't be done if the footer flag is on, once the footer
- // visibility is handled in the new stack.
+ // TODO(b/293167744): This shouldn't be done if the footer flag is on, once the silent
+ // section clear action is handled in the new stack.
controller.setNotifStats(notifStats)
if (NotificationIconContainerRefactor.isEnabled || FooterViewRefactor.isEnabled) {
renderListInteractor.setRenderedList(entries)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt
index 0a9e12a..ccf6f40 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationSettingsInteractorModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index 5111c11..b23ef35 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -52,6 +52,9 @@
isVisible =
activeNotificationsInteractor.hasClearableNotifications
.sample(
+ // TODO(b/322167853): This check is currently duplicated in
+ // NotificationListViewModel, but instead it should be a field in
+ // ShadeAnimationInteractor.
combine(
shadeInteractor.isShadeFullyExpanded,
shadeInteractor.isShadeTouchable,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
index d903f06..8768ea2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
@@ -29,6 +29,8 @@
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
@@ -56,6 +58,8 @@
panelTouchesEnabled && isKeyguardVisible
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** Amount of a "white" tint to be applied to the icons. */
val tintAlpha: Flow<Float> =
@@ -70,6 +74,8 @@
aodAmt + dozeAmt // If transitioning between them, they should sum to 1f
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** Are notification icons animated (ex: animated gif)? */
val areIconAnimationsEnabled: Flow<Boolean> =
@@ -78,8 +84,10 @@
// Don't animate icons when we're on AOD / dozing
it != KeyguardState.AOD && it != KeyguardState.DOZING
}
- .flowOn(bgContext)
.onStart { emit(true) }
+ .flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** [NotificationIconsViewData] indicating which icons to display in the view. */
val icons: Flow<NotificationIconsViewData> =
@@ -91,4 +99,6 @@
)
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
index 3574828..260cccd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
@@ -21,6 +21,8 @@
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -56,4 +58,6 @@
)
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
index 38921c2..a64f888 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -35,6 +35,7 @@
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
@@ -64,10 +65,13 @@
panelTouchesEnabled && !isKeyguardShowing
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** The colors with which to display the notification icons. */
val iconColors: Flow<NotificationIconColorLookup> =
- combine(darkIconInteractor.tintAreas, darkIconInteractor.tintColor) { areas, tint ->
+ darkIconInteractor.darkState
+ .map { (areas: Collection<Rect>, tint: Int) ->
NotificationIconColorLookup { viewBounds: Rect ->
if (DarkIconDispatcher.isInAreas(areas, viewBounds)) {
IconColorsImpl(tint, areas)
@@ -77,6 +81,8 @@
}
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** [NotificationIconsViewData] indicating which icons to display in the view. */
val icons: Flow<NotificationIconsViewData> =
@@ -88,6 +94,8 @@
)
}
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
/** An Icon to show "isolated" in the IconContainer. */
val isolatedIcon: Flow<AnimatedValue<NotificationIconInfo?>> =
@@ -99,6 +107,8 @@
}
.distinctUntilChanged()
.flowOn(bgContext)
+ .conflate()
+ .distinctUntilChanged()
.pairwise(initialValue = null)
.sample(shadeInteractor.shadeExpansion) { (prev, iconInfo), shadeExpansion ->
val animate =
@@ -113,7 +123,7 @@
/** Location to show an isolated icon, if there is one. */
val isolatedIconLocation: Flow<Rect> =
- headsUpIconInteractor.isolatedIconLocation.filterNotNull()
+ headsUpIconInteractor.isolatedIconLocation.filterNotNull().conflate().distinctUntilChanged()
private class IconColorsImpl(
override val tint: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 0a11eb2..42b56e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -752,6 +752,7 @@
}
public void setIsRemoteInputActive(boolean isActive) {
+ FooterViewRefactor.assertInLegacyMode();
mIsRemoteInputActive = isActive;
updateFooter();
}
@@ -764,6 +765,7 @@
@VisibleForTesting
public void updateFooter() {
+ FooterViewRefactor.assertInLegacyMode();
if (mFooterView == null || mController == null) {
return;
}
@@ -776,10 +778,12 @@
}
private boolean shouldShowDismissView() {
+ FooterViewRefactor.assertInLegacyMode();
return mController.hasActiveClearableNotifications(ROWS_ALL);
}
private boolean shouldShowFooterView(boolean showDismissView) {
+ FooterViewRefactor.assertInLegacyMode();
return (showDismissView || mController.getVisibleNotificationCount() > 0)
&& mIsCurrentUserSetup // see: b/193149550
&& !onKeyguard()
@@ -4359,6 +4363,12 @@
layoutEnd -= mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
}
if (endPosition > layoutEnd) {
+ // if Scene Container is active, send bottom notification expansion delta
+ // to it so that it can scroll the stack and scrim accordingly.
+ if (SceneContainerFlag.isEnabled()) {
+ float diff = endPosition - layoutEnd;
+ mController.sendSyntheticScrollToSceneFramework(diff);
+ }
setOwnScrollY((int) (mOwnScrollY + endPosition - layoutEnd));
mDisallowScrollingInThisMotion = true;
}
@@ -4723,9 +4733,6 @@
footerView.setClearAllButtonVisible(false /* visible */, true /* animate */);
});
}
- if (FooterViewRefactor.isEnabled()) {
- updateFooter();
- }
}
public void setEmptyShadeView(EmptyShadeView emptyShadeView) {
@@ -4790,16 +4797,15 @@
}
public void updateFooterView(boolean visible, boolean showDismissView, boolean showHistory) {
+ FooterViewRefactor.assertInLegacyMode();
if (mFooterView == null || mNotificationStackSizeCalculator == null) {
return;
}
boolean animate = mIsExpanded && mAnimationsEnabled;
mFooterView.setVisible(visible, animate);
- if (!FooterViewRefactor.isEnabled()) {
- mFooterView.showHistory(showHistory);
- mFooterView.setClearAllButtonVisible(showDismissView, animate);
- mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications);
- }
+ mFooterView.showHistory(showHistory);
+ mFooterView.setClearAllButtonVisible(showDismissView, animate);
+ mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications);
}
@VisibleForTesting
@@ -5070,7 +5076,7 @@
if (mOwnScrollY > 0) {
setOwnScrollY((int) MathUtils.lerp(mOwnScrollY, 0, mQsExpansionFraction));
}
- if (footerAffected) {
+ if (!FooterViewRefactor.isEnabled() && footerAffected) {
updateFooter();
}
}
@@ -5081,6 +5087,10 @@
}
private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) {
+ // If scene container is active, NSSL should not control its own scrolling.
+ if (SceneContainerFlag.isEnabled()) {
+ return;
+ }
// Avoid Flicking during clear all
// when the shade finishes closing, onExpansionStopped will call
// resetScrollPosition to setOwnScrollY to 0
@@ -5176,6 +5186,7 @@
}
void setUpcomingStatusBarState(int upcomingStatusBarState) {
+ FooterViewRefactor.assertInLegacyMode();
mUpcomingStatusBarState = upcomingStatusBarState;
if (mUpcomingStatusBarState != mStatusBarState) {
updateFooter();
@@ -5193,7 +5204,9 @@
setDimmed(onKeyguard, fromShadeLocked);
setExpandingEnabled(!onKeyguard);
- updateFooter();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateFooter();
+ }
requestChildrenUpdate();
onUpdateRowStates();
updateVisibility();
@@ -5270,8 +5283,11 @@
for (int i = 0; i < childCount; i++) {
ExpandableView child = getChildAtIndex(i);
child.dump(pw, args);
- if (child instanceof FooterView) {
- DumpUtilsKt.withIncreasedIndent(pw, () -> dumpFooterViewVisibility(pw));
+ if (!FooterViewRefactor.isEnabled()) {
+ if (child instanceof FooterView) {
+ DumpUtilsKt.withIncreasedIndent(pw,
+ () -> dumpFooterViewVisibility(pw));
+ }
}
pw.println();
}
@@ -5290,6 +5306,7 @@
}
private void dumpFooterViewVisibility(IndentingPrintWriter pw) {
+ FooterViewRefactor.assertInLegacyMode();
final boolean showDismissView = shouldShowDismissView();
pw.println("showFooterView: " + shouldShowFooterView(showDismissView));
@@ -5988,6 +6005,7 @@
* Sets whether the current user is set up, which is required to show the footer (b/193149550)
*/
public void setCurrentUserSetup(boolean isCurrentUserSetup) {
+ FooterViewRefactor.assertInLegacyMode();
if (mIsCurrentUserSetup != isCurrentUserSetup) {
mIsCurrentUserSetup = isCurrentUserSetup;
updateFooter();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index a2ff406..5fa0624 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -314,8 +314,10 @@
// The bottom might change because we're using the final actual height of the view
mView.setAnimateBottomOnLayout(true);
}
- // Let's update the footer once the notifications have been updated (in the next frame)
- mView.post(this::updateFooter);
+ if (!FooterViewRefactor.isEnabled()) {
+ // Let's update the footer once the notifications have been updated (in the next frame)
+ mView.post(this::updateFooter);
+ }
};
@VisibleForTesting
@@ -342,8 +344,8 @@
mView.reinflateViews();
if (!FooterViewRefactor.isEnabled()) {
updateShowEmptyShadeView();
+ updateFooter();
}
- updateFooter();
}
@Override
@@ -389,7 +391,9 @@
@Override
public void onUpcomingStateChanged(int newState) {
- mView.setUpcomingStatusBarState(newState);
+ if (!FooterViewRefactor.isEnabled()) {
+ mView.setUpcomingStatusBarState(newState);
+ }
}
@Override
@@ -407,7 +411,9 @@
public void onUserChanged(int userId) {
updateSensitivenessWithAnimation(false);
mHistoryEnabled = null;
- updateFooter();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateFooter();
+ }
}
};
@@ -810,14 +816,14 @@
if (!FooterViewRefactor.isEnabled()) {
mView.setFooterClearAllListener(() ->
mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES));
+ mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive());
+ mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() {
+ @Override
+ public void onRemoteInputActive(boolean active) {
+ mView.setIsRemoteInputActive(active);
+ }
+ });
}
- mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive());
- mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() {
- @Override
- public void onRemoteInputActive(boolean active) {
- mView.setIsRemoteInputActive(active);
- }
- });
mView.setClearAllFinishedWhilePanelExpandedRunnable(()-> {
final Runnable doCollapseRunnable = () ->
mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE);
@@ -871,7 +877,9 @@
switch (key) {
case Settings.Secure.NOTIFICATION_HISTORY_ENABLED:
mHistoryEnabled = null; // invalidate
- updateFooter();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateFooter();
+ }
break;
case HIGH_PRIORITY:
mView.setHighPriorityBeforeSpeedBump("1".equals(newValue));
@@ -893,9 +901,11 @@
return kotlin.Unit.INSTANCE;
});
- // attach callback, and then call it to update mView immediately
- mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
- mDeviceProvisionedListener.onDeviceProvisionedChanged();
+ if (!FooterViewRefactor.isEnabled()) {
+ // attach callback, and then call it to update mView immediately
+ mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
+ mDeviceProvisionedListener.onDeviceProvisionedChanged();
+ }
if (screenshareNotificationHiding()) {
mSensitiveNotificationProtectionController
@@ -1106,8 +1116,7 @@
}
public int getVisibleNotificationCount() {
- // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle footer
- // visibility in the refactored code
+ FooterViewRefactor.assertInLegacyMode();
return mNotifStats.getNumActiveNotifs();
}
@@ -1142,6 +1151,11 @@
}
}
+ /** Send internal notification expansion to the scene container framework. */
+ public void sendSyntheticScrollToSceneFramework(Float delta) {
+ mStackAppearanceInteractor.setSyntheticScroll(delta);
+ }
+
/** Get the y-coordinate of the top bound of the stack. */
public float getPlaceholderTop() {
return mStackAppearanceInteractor.getStackBounds().getValue().getTop();
@@ -1509,14 +1523,14 @@
* Return whether there are any clearable notifications
*/
public boolean hasActiveClearableNotifications(@SelectedRows int selection) {
- // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer
- // visibility in the refactored code
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the silent
+ // section clear action in the new stack.
return hasNotifications(selection, true /* clearable */);
}
public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) {
- // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer
- // visibility in the refactored code
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the silent
+ // section clear action in the new stack.
boolean hasAlertingMatchingClearable = isClearable
? mNotifStats.getHasClearableAlertingNotifs()
: mNotifStats.getHasNonClearableAlertingNotifs();
@@ -1558,7 +1572,9 @@
boolean remoteInputActive) {
mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
entry.notifyHeightChanged(true /* needsAnimation */);
- updateFooter();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateFooter();
+ }
}
public void lockScrollTo(NotificationEntry entry) {
@@ -1573,6 +1589,7 @@
}
public void updateFooter() {
+ FooterViewRefactor.assertInLegacyMode();
Trace.beginSection("NSSLC.updateFooter");
mView.updateFooter();
Trace.endSection();
@@ -2134,19 +2151,16 @@
private class NotifStackControllerImpl implements NotifStackController {
@Override
public void setNotifStats(@NonNull NotifStats notifStats) {
- // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once footer visibility
- // is handled in the refactored stack.
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the silent
+ // section clear action in the new stack.
mNotifStats = notifStats;
if (!FooterViewRefactor.isEnabled()) {
mView.setHasFilteredOutSeenNotifications(
mSeenNotificationsInteractor
.getHasFilteredOutSeenNotifications().getValue());
- }
- updateFooter();
-
- if (!FooterViewRefactor.isEnabled()) {
+ updateFooter();
updateShowEmptyShadeView();
updateImportantForAccessibility();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 664a6b6..15fde0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -33,6 +33,7 @@
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.notification.SourceType;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -137,7 +138,7 @@
}
private void updateAlphaState(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
for (ExpandableView view : algorithmState.visibleChildren) {
final ViewState viewState = view.getViewState();
final boolean isHunGoingToShade = ambientState.isShadeExpanded()
@@ -295,7 +296,7 @@
}
private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState,
- int speedBumpIndex) {
+ int speedBumpIndex) {
int childCount = algorithmState.visibleChildren.size();
int belowSpeedBump = speedBumpIndex;
for (int i = 0; i < childCount; i++) {
@@ -322,7 +323,7 @@
}
private void updateClipping(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
float drawStart = ambientState.isOnKeyguard() ? 0
: ambientState.getStackY() - ambientState.getScrollY();
float clipStart = 0;
@@ -454,7 +455,7 @@
}
private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex,
- ExpandableView v) {
+ ExpandableView v) {
ExpandableViewState viewState = v.getViewState();
viewState.notGoneIndex = notGoneIndex;
state.visibleChildren.add(v);
@@ -480,7 +481,7 @@
* @param ambientState The current ambient state
*/
protected void updatePositionsForState(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
if (!ambientState.isOnKeyguard()
|| (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
algorithmState.mCurrentYPosition += mNotificationScrimPadding;
@@ -494,7 +495,7 @@
}
private void setLocation(ExpandableViewState expandableViewState, float currentYPosition,
- int i) {
+ int i) {
expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
if (currentYPosition <= 0) {
expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
@@ -598,18 +599,31 @@
viewEnd, /* hunMax */ ambientState.getMaxHeadsUpTranslation()
);
if (view instanceof FooterView) {
- final boolean shadeClosed = !ambientState.isShadeExpanded();
- final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
- if (shadeClosed) {
- viewState.hidden = true;
- } else {
+ if (FooterViewRefactor.isEnabled()) {
final float footerEnd = algorithmState.mCurrentExpandedYPosition
+ view.getIntrinsicHeight();
final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
+ // TODO(b/293167744): May be able to keep only noSpaceForFooter here if we add an
+ // emission when clearAllNotifications is called, and then use that in the footer
+ // visibility flow.
((FooterView.FooterViewState) viewState).hideContent =
- isShelfShowing || noSpaceForFooter
- || (ambientState.isClearAllInProgress()
+ noSpaceForFooter || (ambientState.isClearAllInProgress()
&& !hasNonClearableNotifs(algorithmState));
+
+ } else {
+ final boolean shadeClosed = !ambientState.isShadeExpanded();
+ final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
+ if (shadeClosed) {
+ viewState.hidden = true;
+ } else {
+ final float footerEnd = algorithmState.mCurrentExpandedYPosition
+ + view.getIntrinsicHeight();
+ final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
+ ((FooterView.FooterViewState) viewState).hideContent =
+ isShelfShowing || noSpaceForFooter
+ || (ambientState.isClearAllInProgress()
+ && !hasNonClearableNotifs(algorithmState));
+ }
}
} else {
if (view instanceof EmptyShadeView) {
@@ -731,7 +745,7 @@
@VisibleForTesting
void updatePulsingStates(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
int childCount = algorithmState.visibleChildren.size();
ExpandableNotificationRow pulsingRow = null;
for (int i = 0; i < childCount; i++) {
@@ -761,7 +775,7 @@
}
private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
int childCount = algorithmState.visibleChildren.size();
// Move the tracked heads up into position during the appear animation, by interpolating
@@ -870,18 +884,18 @@
boolean shouldHunBeVisibleWhenScrolled(boolean mustStayOnScreen, boolean headsUpIsVisible,
boolean showingPulsing, boolean isOnKeyguard, boolean headsUpOnKeyguard) {
return mustStayOnScreen && !headsUpIsVisible
- && !showingPulsing
- && (!isOnKeyguard || headsUpOnKeyguard);
+ && !showingPulsing
+ && (!isOnKeyguard || headsUpOnKeyguard);
}
- /**
+ /**
* When shade is open and we are scrolled to the bottom of notifications,
* clamp incoming HUN in its collapsed form, right below qs offset.
* Transition pinned collapsed HUN to full height when scrolling back up.
*/
@VisibleForTesting
void clampHunToTop(float clampInset, float stackTranslation, float collapsedHeight,
- ExpandableViewState viewState) {
+ ExpandableViewState viewState) {
final float newTranslation = Math.max(clampInset + stackTranslation,
viewState.getYTranslation());
@@ -896,7 +910,7 @@
// Pin HUN to bottom of expanded QS
// while the rest of notifications are scrolled offscreen.
private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
- ExpandableViewState childState) {
+ ExpandableViewState childState) {
float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
+ ambientState.getStackTranslation();
@@ -919,7 +933,7 @@
@VisibleForTesting
float computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY,
- float viewMaxHeight, float originalCornerRadius) {
+ float viewMaxHeight, float originalCornerRadius) {
// Compute y where corner roundness should be in its original unpinned state.
// We use view max height because the pinned collapsed HUN expands to max height
@@ -948,7 +962,7 @@
* @param ambientState The ambient state of the algorithm
*/
private void updateZValuesForState(StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState) {
int childCount = algorithmState.visibleChildren.size();
float childrenOnTop = 0.0f;
@@ -976,13 +990,13 @@
* vertically top of screen. Top HUNs should have drop shadows
* @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated
* @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height
- * that overlaps with QQS Panel. The integer part represents the count of
- * previous HUNs whose Z positions are greater than 0.
+ * that overlaps with QQS Panel. The integer part represents the count of
+ * previous HUNs whose Z positions are greater than 0.
*/
protected float updateChildZValue(int i, float childrenOnTop,
- StackScrollAlgorithmState algorithmState,
- AmbientState ambientState,
- boolean isTopHun) {
+ StackScrollAlgorithmState algorithmState,
+ AmbientState ambientState,
+ boolean isTopHun) {
ExpandableView child = algorithmState.visibleChildren.get(i);
ExpandableViewState childViewState = child.getViewState();
float baseZ = ambientState.getBaseZHeight();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
index 311ba83..9efe632 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
@@ -47,4 +47,11 @@
* further.
*/
val scrolledToTop = MutableStateFlow(true)
+
+ /**
+ * The amount in px that the notification stack should scroll due to internal expansion. This
+ * should only happen when a notification expansion hits the bottom of the screen, so it is
+ * necessary to scroll up to keep expanding the notification.
+ */
+ val syntheticScroll = MutableStateFlow(0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 9984ba9..08df473 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -21,6 +21,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository
import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -50,6 +51,13 @@
*/
val scrolledToTop: StateFlow<Boolean> = repository.scrolledToTop.asStateFlow()
+ /**
+ * The amount in px that the notification stack should scroll due to internal expansion. This
+ * should only happen when a notification expansion hits the bottom of the screen, so it is
+ * necessary to scroll up to keep expanding the notification.
+ */
+ val syntheticScroll: Flow<Float> = repository.syntheticScroll.asStateFlow()
+
/** Sets the position of the notification stack in the current scene. */
fun setStackBounds(bounds: NotificationContainerBounds) {
check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
@@ -70,4 +78,9 @@
fun setScrolledToTop(scrolledToTop: Boolean) {
repository.scrolledToTop.value = scrolledToTop
}
+
+ /** Sets the amount (px) that the notification stack should scroll due to internal expansion. */
+ fun setSyntheticScroll(delta: Float) {
+ repository.syntheticScroll.value = delta
+ }
}
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 4d65b9d..883aa9b 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
@@ -18,11 +18,10 @@
import android.view.LayoutInflater
import androidx.lifecycle.lifecycleScope
-import com.android.app.tracing.traceSection
+import com.android.app.tracing.TraceUtils.traceAsync
import com.android.internal.logging.MetricsLogger
import com.android.internal.logging.nano.MetricsProto
import com.android.systemui.common.ui.ConfigurationState
-import com.android.systemui.common.ui.reinflateAndBindLatest
import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.lifecycle.repeatWhenAttached
@@ -33,6 +32,7 @@
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
+import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
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
@@ -43,12 +43,18 @@
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
import com.android.systemui.statusbar.phone.NotificationIconAreaController
+import com.android.systemui.util.kotlin.awaitCancellationThenDispose
import com.android.systemui.util.kotlin.getOrNull
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.value
import java.util.Optional
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
/** Binds a [NotificationStackScrollLayout] to its [view model][NotificationListViewModel]. */
@@ -83,7 +89,7 @@
bindHideList(viewController, viewModel, hiderTracker)
if (FooterViewRefactor.isEnabled) {
- launch { bindFooter(view) }
+ launch { reinflateAndBindFooter(view) }
launch { bindEmptyShade(view) }
launch {
viewModel.isImportantForAccessibility.collect { isImportantForAccessibility
@@ -108,42 +114,61 @@
)
}
- private suspend fun bindFooter(parentView: NotificationStackScrollLayout) {
+ private suspend fun reinflateAndBindFooter(parentView: NotificationStackScrollLayout) {
viewModel.footer.getOrNull()?.let { footerViewModel ->
// The footer needs to be re-inflated every time the theme or the font size changes.
- configuration.reinflateAndBindLatest(
- R.layout.status_bar_notification_footer,
- parentView,
- attachToRoot = false,
- backgroundDispatcher,
- ) { footerView: FooterView ->
- traceSection("bind FooterView") {
- val disposableHandle =
- FooterViewBinder.bindWhileAttached(
- footerView,
- footerViewModel,
- clearAllNotifications = {
- metricsLogger.action(
- MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
- )
- parentView.clearAllNotifications()
- },
- launchNotificationSettings = { view ->
- notificationActivityStarter
- .get()
- .startHistoryIntent(view, /* showHistory = */ false)
- },
- launchNotificationHistory = { view ->
- notificationActivityStarter
- .get()
- .startHistoryIntent(view, /* showHistory = */ true)
- },
- )
- parentView.setFooterView(footerView)
- return@reinflateAndBindLatest disposableHandle
+ configuration
+ .inflateLayout<FooterView>(
+ R.layout.status_bar_notification_footer,
+ parentView,
+ attachToRoot = false,
+ )
+ .flowOn(backgroundDispatcher)
+ .collectLatest { footerView: FooterView ->
+ traceAsync("bind FooterView") {
+ parentView.setFooterView(footerView)
+ bindFooter(footerView, footerViewModel, parentView)
+ }
}
+ }
+ }
+
+ /**
+ * Binds the footer (including its visibility) and dispose of the [DisposableHandle] when done.
+ */
+ private suspend fun bindFooter(
+ footerView: FooterView,
+ footerViewModel: FooterViewModel,
+ parentView: NotificationStackScrollLayout
+ ): Unit = coroutineScope {
+ val disposableHandle =
+ FooterViewBinder.bindWhileAttached(
+ footerView,
+ footerViewModel,
+ clearAllNotifications = {
+ metricsLogger.action(MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES)
+ parentView.clearAllNotifications()
+ },
+ launchNotificationSettings = { view ->
+ notificationActivityStarter
+ .get()
+ .startHistoryIntent(view, /* showHistory = */ false)
+ },
+ launchNotificationHistory = { view ->
+ notificationActivityStarter
+ .get()
+ .startHistoryIntent(view, /* showHistory = */ true)
+ },
+ )
+ launch {
+ viewModel.shouldShowFooterView.collect { animatedVisibility ->
+ footerView.setVisible(
+ /* visible = */ animatedVisibility.value,
+ /* animate = */ animatedVisibility.isAnimating,
+ )
}
}
+ disposableHandle.awaitCancellationThenDispose()
}
private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
index 814146c..a157785 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
@@ -69,6 +69,9 @@
controller.setMaxAlphaForExpansion(
((expandFraction - 0.5f) / 0.5f).coerceAtLeast(0f)
)
+ if (expandFraction == 0f || expandFraction == 1f) {
+ controller.onExpansionStopped()
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 86c0a678..a6c6586 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -16,21 +16,32 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
+import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.util.kotlin.combine
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.ui.AnimatableEvent
+import com.android.systemui.util.ui.AnimatedValue
+import com.android.systemui.util.ui.toAnimatedValueFlow
import java.util.Optional
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
/** ViewModel for the list of notifications. */
@@ -42,9 +53,13 @@
val footer: Optional<FooterViewModel>,
val logger: Optional<NotificationLoggerViewModel>,
activeNotificationsInteractor: ActiveNotificationsInteractor,
+ keyguardInteractor: KeyguardInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ powerInteractor: PowerInteractor,
+ remoteInputInteractor: RemoteInputInteractor,
seenNotificationsInteractor: SeenNotificationsInteractor,
shadeInteractor: ShadeInteractor,
+ userSetupInteractor: UserSetupInteractor,
zenModeInteractor: ZenModeInteractor,
) {
/**
@@ -76,6 +91,10 @@
combine(
activeNotificationsInteractor.areAnyNotificationsPresent,
shadeInteractor.isQsFullscreen,
+ // TODO(b/293167744): It looks like we're essentially trying to check the same
+ // things for the empty shade visibility as we do for the footer, just in a
+ // slightly different way. We should change this so we also check
+ // statusBarState and isAwake instead of specific keyguard transitions.
keyguardTransitionInteractor.isInTransitionToState(KeyguardState.AOD).onStart {
emit(false)
},
@@ -97,6 +116,80 @@
}
}
+ val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(AnimatedValue.NotAnimating(false))
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ userSetupInteractor.isUserSetUp,
+ keyguardInteractor.statusBarState.map { it == StatusBarState.KEYGUARD },
+ shadeInteractor.qsExpansion,
+ shadeInteractor.isQsFullscreen,
+ powerInteractor.isAsleep,
+ remoteInputInteractor.isRemoteInputActive,
+ shadeInteractor.shadeExpansion.map { it == 0f }
+ ) {
+ hasNotifications,
+ isUserSetUp,
+ isOnKeyguard,
+ qsExpansion,
+ qsFullScreen,
+ isAsleep,
+ isRemoteInputActive,
+ isShadeClosed ->
+ Pair(
+ // Should the footer be visible?
+ when {
+ !hasNotifications -> false
+ // Hide the footer until the user setup is complete, to prevent access
+ // to settings (b/193149550).
+ !isUserSetUp -> false
+ // Do not show the footer if the lockscreen is visible (incl. AOD),
+ // except if the shade is opened on top. See also b/219680200.
+ isOnKeyguard -> false
+ // Make sure we're not showing the footer in the transition to AOD while
+ // going to sleep (b/190227875). The StatusBarState is unfortunately not
+ // updated quickly enough when the power button is pressed, so this is
+ // necessary in addition to the isOnKeyguard check.
+ isAsleep -> false
+ // Do not show the footer if quick settings are fully expanded (except
+ // for the foldable split shade view). See b/201427195 && b/222699879.
+ qsExpansion == 1f && qsFullScreen -> false
+ // Hide the footer if remote input is active (i.e. user is replying to a
+ // notification). See b/75984847.
+ isRemoteInputActive -> false
+ // Never show the footer if the shade is collapsed (e.g. when HUNing).
+ isShadeClosed -> false
+ else -> true
+ },
+ // This could in theory be in the .sample below, but it tends to be
+ // inconsistent, so we're passing it on to make sure we have the same state.
+ isOnKeyguard
+ )
+ }
+ .distinctUntilChanged()
+ // Should we animate the visibility change?
+ .sample(
+ // TODO(b/322167853): This check is currently duplicated in FooterViewModel,
+ // but instead it should be a field in ShadeAnimationInteractor.
+ combine(
+ shadeInteractor.isShadeFullyExpanded,
+ shadeInteractor.isShadeTouchable,
+ ::Pair
+ )
+ .onStart { emit(Pair(false, false)) }
+ ) { (visible, isOnKeyguard), (isShadeFullyExpanded, animationsEnabled) ->
+ // Animate if the shade is interactive, but NOT on the lockscreen. Having
+ // animations enabled while on the lockscreen makes the footer appear briefly
+ // when transitioning between the shade and keyguard.
+ val shouldAnimate = isShadeFullyExpanded && animationsEnabled && !isOnKeyguard
+ AnimatableEvent(visible, shouldAnimate)
+ }
+ .toAnimatedValueFlow()
+ }
+ }
+
// TODO(b/308591475): This should be tracked separately by the empty shade.
val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
index bdf1a64..3a0f03f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
@@ -28,6 +28,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
/** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
@SysUISingleton
@@ -45,31 +46,32 @@
*/
val expandFraction: Flow<Float> =
combine(
- shadeInteractor.shadeExpansion,
- sceneInteractor.transitionState,
- ) { shadeExpansion, transitionState ->
- when (transitionState) {
- is ObservableTransitionState.Idle -> {
- if (transitionState.scene == SceneKey.Lockscreen) {
- 1f
- } else {
- shadeExpansion
+ shadeInteractor.shadeExpansion,
+ sceneInteractor.transitionState,
+ ) { shadeExpansion, transitionState ->
+ when (transitionState) {
+ is ObservableTransitionState.Idle -> {
+ if (transitionState.scene == SceneKey.Lockscreen) {
+ 1f
+ } else {
+ shadeExpansion
+ }
}
- }
- is ObservableTransitionState.Transition -> {
- if (
- (transitionState.fromScene == SceneKey.Shade &&
- transitionState.toScene == SceneKey.QuickSettings) ||
- (transitionState.fromScene == SceneKey.QuickSettings &&
- transitionState.toScene == SceneKey.Shade)
- ) {
- 1f
- } else {
- shadeExpansion
+ is ObservableTransitionState.Transition -> {
+ if (
+ (transitionState.fromScene == SceneKey.Shade &&
+ transitionState.toScene == SceneKey.QuickSettings) ||
+ (transitionState.fromScene == SceneKey.QuickSettings &&
+ transitionState.toScene == SceneKey.Shade)
+ ) {
+ 1f
+ } else {
+ shadeExpansion
+ }
}
}
}
- }
+ .distinctUntilChanged()
/** The bounds of the notification stack in the current scene. */
val stackBounds: Flow<NotificationContainerBounds> = stackAppearanceInteractor.stackBounds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 65d9c9f..7ac5cd4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -86,6 +86,13 @@
*/
val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion
+ /**
+ * The amount in px that the notification stack should scroll due to internal expansion. This
+ * should only happen when a notification expansion hits the bottom of the screen, so it is
+ * necessary to scroll up to keep expanding the notification.
+ */
+ val syntheticScroll: Flow<Float> = interactor.syntheticScroll
+
/** Sets the y-coord in px of the top of the contents of the notification stack. */
fun onContentTopChanged(padding: Float) {
interactor.setContentTop(padding)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 8a56da3..b49af0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -837,7 +837,7 @@
if (animationController == null) {
return null
}
- val rootView = animationController.launchContainer.rootView
+ val rootView = animationController.transitionContainer.rootView
val controllerFromStatusBar: Optional<ActivityLaunchAnimator.Controller> =
statusBarWindowController.wrapAnimationControllerIfInStatusBar(
rootView,
@@ -881,8 +881,8 @@
}
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
- super.onLaunchAnimationStart(isExpandingFullyAbove)
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ super.onTransitionAnimationStart(isExpandingFullyAbove)
// Double check that the keyguard is still showing and not going
// away, but if so set the keyguard occluded. Typically, WM will let
@@ -902,14 +902,14 @@
}
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
// Set mIsLaunchingActivityOverLockscreen to false before actually
// finishing the animation so that we can assume that
// mIsLaunchingActivityOverLockscreen being true means that we will
// collapse the shade (or at least run the post collapse runnables)
// later on.
centralSurfaces?.setIsLaunchingActivityOverLockscreen(false)
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
}
override fun onLaunchAnimationCancelled(newKeyguardOccludedState: Boolean?) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index be5c6b3..8e3d678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -349,8 +349,12 @@
}
}
if (child instanceof StatusBarIconView) {
- ((StatusBarIconView) child).updateIconDimens();
- if (!NotificationIconContainerRefactor.isEnabled()) {
+ if (NotificationIconContainerRefactor.isEnabled()) {
+ if (!mChangingViewPositions) {
+ ((StatusBarIconView) child).updateIconDimens();
+ }
+ } else {
+ ((StatusBarIconView) child).updateIconDimens();
((StatusBarIconView) child).setDozing(mDozing, false, 0);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
index 8ca5bfc..d43f470 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -2,7 +2,7 @@
import android.view.View
import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.animation.LaunchAnimator
+import com.android.systemui.animation.TransitionAnimator
import com.android.systemui.shade.ShadeController
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor
@@ -34,8 +34,8 @@
}
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
shadeAnimationInteractor.setIsLaunchingActivity(true)
if (!isExpandingFullyAbove) {
shadeViewController.collapseWithDuration(
@@ -43,18 +43,18 @@
}
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
- delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+ delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
shadeAnimationInteractor.setIsLaunchingActivity(false)
shadeController.onLaunchAnimationEnd(isExpandingFullyAbove)
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
- delegate.onLaunchAnimationProgress(state, progress, linearProgress)
+ delegate.onTransitionAnimationProgress(state, progress, linearProgress)
shadeViewController.applyLaunchAnimationProgress(linearProgress)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt
index 246645e..72f4540 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt
@@ -15,20 +15,14 @@
*/
package com.android.systemui.statusbar.phone.domain.interactor
-import android.graphics.Rect
import com.android.systemui.statusbar.phone.data.repository.DarkIconRepository
+import com.android.systemui.statusbar.phone.domain.model.DarkState
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/** States pertaining to calculating colors for icons in dark mode. */
class DarkIconInteractor @Inject constructor(repository: DarkIconRepository) {
- /** @see com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange.areas */
- val tintAreas: Flow<Collection<Rect>> = repository.darkState.map { it.areas }
- /**
- * @see com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange.darkIntensity
- */
- val darkIntensity: Flow<Float> = repository.darkState.map { it.darkIntensity }
- /** @see com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange.tint */
- val tintColor: Flow<Int> = repository.darkState.map { it.tint }
+ /** Dark-mode state for tinting icons. */
+ val darkState: Flow<DarkState> = repository.darkState.map { DarkState(it.areas, it.tint) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt
new file mode 100644
index 0000000..3cab7cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.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.statusbar.phone.domain.model
+
+import android.graphics.Rect
+
+/** Dark mode visual states. */
+data class DarkState(
+ /** Areas on screen that require a dark-mode adjustment. */
+ val areas: Collection<Rect>,
+ /** Tint color to apply to UI elements that fall within [areas]. */
+ val tint: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
index 63566ee..e1798d3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.pipeline.satellite.ui.model
+import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.res.R
import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
@@ -36,7 +37,10 @@
SatelliteConnectionState.On ->
Icon.Resource(
res = R.drawable.ic_satellite_not_connected,
- contentDescription = null,
+ contentDescription =
+ ContentDescription.Resource(
+ R.string.accessibility_status_bar_satellite_available
+ ),
)
SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength)
}
@@ -51,15 +55,36 @@
// TODO(b/316634365): these need content descriptions
when (signalStrength) {
// No signal
- 0 -> Icon.Resource(res = R.drawable.ic_satellite_connected_0, contentDescription = null)
+ 0 ->
+ Icon.Resource(
+ res = R.drawable.ic_satellite_connected_0,
+ contentDescription =
+ ContentDescription.Resource(
+ R.string.accessibility_status_bar_satellite_no_connection
+ )
+ )
// Poor -> Moderate
1,
- 2 -> Icon.Resource(res = R.drawable.ic_satellite_connected_1, contentDescription = null)
+ 2 ->
+ Icon.Resource(
+ res = R.drawable.ic_satellite_connected_1,
+ contentDescription =
+ ContentDescription.Resource(
+ R.string.accessibility_status_bar_satellite_poor_connection
+ )
+ )
// Good -> Great
3,
- 4 -> Icon.Resource(res = R.drawable.ic_satellite_connected_2, contentDescription = null)
+ 4 ->
+ Icon.Resource(
+ res = R.drawable.ic_satellite_connected_2,
+ contentDescription =
+ ContentDescription.Resource(
+ R.string.accessibility_status_bar_satellite_good_connection
+ )
+ )
else -> null
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
index b598782..21d3fa4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
@@ -47,13 +47,13 @@
import android.view.WindowManager;
import com.android.internal.policy.SystemBarUtils;
-import com.android.systemui.res.R;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DelegateLaunchAnimatorController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.util.JankMonitorTransitionProgressListener;
@@ -194,17 +194,17 @@
return Optional.empty();
}
- animationController.setLaunchContainer(mLaunchAnimationContainer);
+ animationController.setTransitionContainer(mLaunchAnimationContainer);
return Optional.of(new DelegateLaunchAnimatorController(animationController) {
@Override
- public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {
- getDelegate().onLaunchAnimationStart(isExpandingFullyAbove);
+ public void onTransitionAnimationStart(boolean isExpandingFullyAbove) {
+ getDelegate().onTransitionAnimationStart(isExpandingFullyAbove);
setLaunchAnimationRunning(true);
}
@Override
- public void onLaunchAnimationEnd(boolean isExpandingFullyAbove) {
- getDelegate().onLaunchAnimationEnd(isExpandingFullyAbove);
+ public void onTransitionAnimationEnd(boolean isExpandingFullyAbove) {
+ getDelegate().onTransitionAnimationEnd(isExpandingFullyAbove);
setLaunchAnimationRunning(false);
}
});
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
new file mode 100644
index 0000000..5c53ff9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.unfold
+
+import android.animation.ValueAnimator
+import android.annotation.BinderThread
+import android.content.Context
+import android.os.Handler
+import android.os.SystemProperties
+import android.util.Log
+import android.view.animation.DecelerateInterpolator
+import androidx.core.animation.addListener
+import com.android.internal.foldables.FoldLockSettingAvailabilityProvider
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.data.repository.DeviceStateRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState
+import com.android.systemui.statusbar.LinearSideLightRevealEffect
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_OPAQUE
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_TRANSPARENT
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation
+import com.android.systemui.unfold.dagger.UnfoldBg
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
+
+class FoldLightRevealOverlayAnimation
+@Inject
+constructor(
+ private val context: Context,
+ @UnfoldBg private val bgHandler: Handler,
+ private val deviceStateRepository: DeviceStateRepository,
+ private val powerInteractor: PowerInteractor,
+ @Background private val applicationScope: CoroutineScope,
+ private val animationStatusRepository: AnimationStatusRepository,
+ private val controllerFactory: FullscreenLightRevealAnimationController.Factory
+) : FullscreenLightRevealAnimation {
+
+ private val revealProgressValueAnimator: ValueAnimator =
+ ValueAnimator.ofFloat(ALPHA_OPAQUE, ALPHA_TRANSPARENT)
+ private lateinit var controller: FullscreenLightRevealAnimationController
+ @Volatile private var readyCallback: CompletableDeferred<Runnable>? = null
+
+ override fun init() {
+ // This method will be called only on devices where this animation is enabled,
+ // so normally this thread won't be created
+ if (!FoldLockSettingAvailabilityProvider(context.resources).isFoldLockBehaviorAvailable) {
+ return
+ }
+
+ controller =
+ controllerFactory.create(
+ displaySelector = { minByOrNull { it.naturalWidth } },
+ effectFactory = { LinearSideLightRevealEffect(it.isVerticalRotation()) },
+ overlayContainerName = SURFACE_CONTAINER_NAME
+ )
+ controller.init()
+
+ applicationScope.launch(bgHandler.asCoroutineDispatcher()) {
+ powerInteractor.screenPowerState.collect {
+ if (it == ScreenPowerState.SCREEN_ON) {
+ readyCallback = null
+ }
+ }
+ }
+
+ applicationScope.launch(bgHandler.asCoroutineDispatcher()) {
+ deviceStateRepository.state
+ .map { it != DeviceStateRepository.DeviceState.FOLDED }
+ .distinctUntilChanged()
+ .filter { isUnfolded -> isUnfolded }
+ .collect { controller.ensureOverlayRemoved() }
+ }
+
+ applicationScope.launch(bgHandler.asCoroutineDispatcher()) {
+ deviceStateRepository.state
+ .filter {
+ animationStatusRepository.areAnimationsEnabled().first() &&
+ it == DeviceStateRepository.DeviceState.FOLDED
+ }
+ .collect {
+ try {
+ withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) {
+ readyCallback = CompletableDeferred()
+ val onReady = readyCallback?.await()
+ readyCallback = null
+ controller.addOverlay(ALPHA_OPAQUE, onReady)
+ waitForScreenTurnedOn()
+ playFoldLightRevealOverlayAnimation()
+ }
+ } catch (e: TimeoutCancellationException) {
+ Log.e(TAG, "Fold light reveal animation timed out")
+ ensureOverlayRemovedInternal()
+ }
+ }
+ }
+ }
+
+ @BinderThread
+ override fun onScreenTurningOn(onOverlayReady: Runnable) {
+ readyCallback?.complete(onOverlayReady) ?: onOverlayReady.run()
+ }
+
+ private suspend fun waitForScreenTurnedOn() {
+ powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first()
+ }
+
+ private fun ensureOverlayRemovedInternal() {
+ revealProgressValueAnimator.cancel()
+ controller.ensureOverlayRemoved()
+ }
+
+ private fun playFoldLightRevealOverlayAnimation() {
+ revealProgressValueAnimator.duration = ANIMATION_DURATION
+ revealProgressValueAnimator.interpolator = DecelerateInterpolator()
+ revealProgressValueAnimator.addUpdateListener { animation ->
+ controller.updateRevealAmount(animation.animatedFraction)
+ }
+ revealProgressValueAnimator.addListener(onEnd = { controller.ensureOverlayRemoved() })
+ revealProgressValueAnimator.start()
+ }
+
+ private companion object {
+ const val TAG = "FoldLightRevealOverlayAnimation"
+ const val WAIT_FOR_ANIMATION_TIMEOUT_MS = 2000L
+ const val SURFACE_CONTAINER_NAME = "fold-overlay-container"
+ val ANIMATION_DURATION: Long
+ get() = SystemProperties.getLong("persist.fold_animation_duration", 200L)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt
new file mode 100644
index 0000000..668b143
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt
@@ -0,0 +1,271 @@
+/*
+ * 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.unfold
+
+import android.content.Context
+import android.graphics.PixelFormat
+import android.hardware.display.DisplayManager
+import android.os.Handler
+import android.os.Looper
+import android.os.Trace
+import android.view.Choreographer
+import android.view.Display
+import android.view.DisplayInfo
+import android.view.Surface
+import android.view.Surface.Rotation
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.SurfaceSession
+import android.view.WindowManager
+import android.view.WindowlessWindowManager
+import com.android.app.tracing.traceSection
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.DisplayTracker
+import com.android.systemui.statusbar.LightRevealEffect
+import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.unfold.dagger.UnfoldBg
+import com.android.systemui.unfold.updates.RotationChangeProvider
+import com.android.systemui.util.concurrency.ThreadFactory
+import com.android.wm.shell.displayareahelper.DisplayAreaHelper
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.lang.IllegalArgumentException
+import java.util.Optional
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+
+interface FullscreenLightRevealAnimation {
+ fun init()
+
+ fun onScreenTurningOn(onOverlayReady: Runnable)
+}
+
+class FullscreenLightRevealAnimationController
+@AssistedInject
+constructor(
+ private val context: Context,
+ private val displayManager: DisplayManager,
+ private val threadFactory: ThreadFactory,
+ @UnfoldBg private val bgHandler: Handler,
+ @UnfoldBg private val rotationChangeProvider: RotationChangeProvider,
+ private val displayAreaHelper: Optional<DisplayAreaHelper>,
+ private val displayTracker: DisplayTracker,
+ @Background private val applicationScope: CoroutineScope,
+ @Main private val executor: Executor,
+ @Assisted private val displaySelector: Sequence<DisplayInfo>.() -> DisplayInfo?,
+ @Assisted private val lightRevealEffectFactory: (rotation: Int) -> LightRevealEffect,
+ @Assisted private val overlayContainerName: String
+) {
+
+ private lateinit var bgExecutor: Executor
+ private lateinit var wwm: WindowlessWindowManager
+
+ private var currentRotation: Int = context.display.rotation
+ private var root: SurfaceControlViewHost? = null
+ private var scrimView: LightRevealScrim? = null
+
+ private val rotationWatcher = RotationWatcher()
+ private val internalDisplayInfos: Sequence<DisplayInfo>
+ get() =
+ displayManager
+ .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)
+ .asSequence()
+ .map { DisplayInfo().apply { it.getDisplayInfo(this) } }
+ .filter { it.type == Display.TYPE_INTERNAL }
+
+ var isTouchBlocked: Boolean = false
+ set(value) {
+ if (value != field) {
+ traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) }
+ field = value
+ }
+ }
+
+ fun init() {
+ bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler)
+ rotationChangeProvider.addCallback(rotationWatcher)
+
+ buildSurface { builder ->
+ applicationScope.launch(executor.asCoroutineDispatcher()) {
+ val overlayContainer = builder.build()
+
+ SurfaceControl.Transaction()
+ .setLayer(overlayContainer, OVERLAY_LAYER_Z_INDEX)
+ .show(overlayContainer)
+ .apply()
+
+ wwm =
+ WindowlessWindowManager(context.resources.configuration, overlayContainer, null)
+ }
+ }
+ }
+
+ fun addOverlay(
+ initialAlpha: Float,
+ onOverlayReady: Runnable? = null,
+ ) {
+ if (!::wwm.isInitialized) {
+ // Surface overlay is not created yet on the first SysUI launch
+ onOverlayReady?.run()
+ return
+ }
+ ensureInBackground()
+ ensureOverlayRemoved()
+ prepareOverlay(onOverlayReady, wwm, bgExecutor, initialAlpha)
+ }
+
+ fun ensureOverlayRemoved() {
+ ensureInBackground()
+
+ traceSection("ensureOverlayRemoved") {
+ root?.release()
+ root = null
+ scrimView = null
+ }
+ }
+
+ fun isOverlayVisible(): Boolean {
+ return scrimView == null
+ }
+
+ fun updateRevealAmount(revealAmount: Float) {
+ scrimView?.revealAmount = revealAmount
+ }
+
+ private fun buildSurface(onUpdated: Consumer<SurfaceControl.Builder>) {
+ val containerBuilder =
+ SurfaceControl.Builder(SurfaceSession())
+ .setContainerLayer()
+ .setName(overlayContainerName)
+
+ displayAreaHelper
+ .get()
+ .attachToRootDisplayArea(displayTracker.defaultDisplayId, containerBuilder, onUpdated)
+ }
+
+ private fun prepareOverlay(
+ onOverlayReady: Runnable? = null,
+ wwm: WindowlessWindowManager,
+ bgExecutor: Executor,
+ initialAlpha: Float,
+ ) {
+ val newRoot = SurfaceControlViewHost(context, context.display, wwm, javaClass.simpleName)
+
+ val params = getLayoutParams()
+ val newView =
+ LightRevealScrim(
+ context,
+ attrs = null,
+ initialWidth = params.width,
+ initialHeight = params.height
+ )
+ .apply {
+ revealEffect = lightRevealEffectFactory(currentRotation)
+ revealAmount = initialAlpha
+ }
+
+ newRoot.setView(newView, params)
+
+ if (onOverlayReady != null) {
+ Trace.beginAsyncSection("$TAG#relayout", 0)
+
+ newRoot.relayout(params) { transaction ->
+ val vsyncId = Choreographer.getSfInstance().vsyncId
+ transaction.setFrameTimelineVsync(vsyncId).apply()
+
+ transaction
+ .setFrameTimelineVsync(vsyncId + 1)
+ .addTransactionCommittedListener(bgExecutor) {
+ Trace.endAsyncSection("$TAG#relayout", 0)
+ onOverlayReady.run()
+ }
+ .apply()
+ }
+ }
+ root = newRoot
+ scrimView = newView
+ }
+
+ private fun ensureInBackground() {
+ check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" }
+ }
+
+ private fun getLayoutParams(): WindowManager.LayoutParams {
+ val displayInfo =
+ internalDisplayInfos.displaySelector()
+ ?: throw IllegalArgumentException("No internal displays found!")
+ return WindowManager.LayoutParams().apply {
+ if (currentRotation.isVerticalRotation()) {
+ height = displayInfo.naturalHeight
+ width = displayInfo.naturalWidth
+ } else {
+ height = displayInfo.naturalWidth
+ width = displayInfo.naturalHeight
+ }
+ format = PixelFormat.TRANSLUCENT
+ type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
+ title = javaClass.simpleName
+ layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ fitInsetsTypes = 0
+
+ flags =
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ setTrustedOverlay()
+
+ packageName = context.opPackageName
+ }
+ }
+
+ private inner class RotationWatcher : RotationChangeProvider.RotationListener {
+ override fun onRotationChanged(newRotation: Int) {
+ traceSection("$TAG#onRotationChanged") {
+ if (currentRotation != newRotation) {
+ currentRotation = newRotation
+ scrimView?.revealEffect = lightRevealEffectFactory(currentRotation)
+ root?.relayout(getLayoutParams())
+ }
+ }
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ displaySelector: Sequence<DisplayInfo>.() -> DisplayInfo?,
+ effectFactory: (rotation: Int) -> LightRevealEffect,
+ overlayContainerName: String
+ ): FullscreenLightRevealAnimationController
+ }
+
+ companion object {
+ private const val TAG = "FullscreenLightRevealAnimation"
+ private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE
+ private const val OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1
+ const val ALPHA_TRANSPARENT = 1f
+ const val ALPHA_OPAQUE = 0f
+
+ fun @receiver:Rotation Int.isVerticalRotation(): Boolean =
+ this == Surface.ROTATION_0 || this == Surface.ROTATION_180
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
index 0016d95..139ac7e 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
@@ -17,6 +17,7 @@
package com.android.systemui.unfold
import com.android.keyguard.KeyguardUnfoldTransition
+import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.shade.NotificationPanelUnfoldAnimationController
import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController
@@ -25,10 +26,14 @@
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityManager
import com.android.systemui.util.kotlin.getOrNull
+import dagger.Binds
import dagger.BindsInstance
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
import java.util.Optional
import javax.inject.Named
import javax.inject.Scope
@@ -70,8 +75,33 @@
}
}
+@Module
+interface SysUIUnfoldStartableModule {
+ @Binds
+ @IntoMap
+ @ClassKey(UnfoldInitializationStartable::class)
+ fun bindsUnfoldInitializationStartable(impl: UnfoldInitializationStartable): CoreStartable
+}
+
+@Module
+abstract class SysUIUnfoldInternalModule {
+ @Binds
+ @IntoSet
+ @SysUIUnfoldScope
+ abstract fun bindsUnfoldLightRevealOverlayAnimation(
+ anim: UnfoldLightRevealOverlayAnimation
+ ): FullscreenLightRevealAnimation
+
+ @Binds
+ @IntoSet
+ @SysUIUnfoldScope
+ abstract fun bindsFoldLightRevealOverlayAnimation(
+ anim: FoldLightRevealOverlayAnimation
+ ): FullscreenLightRevealAnimation
+}
+
@SysUIUnfoldScope
-@Subcomponent
+@Subcomponent(modules = [SysUIUnfoldInternalModule::class])
interface SysUIUnfoldComponent {
@Subcomponent.Factory
@@ -92,12 +122,12 @@
fun getFoldAodAnimationController(): FoldAodAnimationController
+ fun getFullScreenLightRevealAnimations(): Set<FullscreenLightRevealAnimation>
+
fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController
fun getUnfoldHapticsPlayer(): UnfoldHapticsPlayer
- fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation
-
fun getUnfoldKeyguardVisibilityManager(): UnfoldKeyguardVisibilityManager
fun getUnfoldLatencyTracker(): UnfoldLatencyTracker
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldInitializationStartable.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldInitializationStartable.kt
new file mode 100644
index 0000000..75d8a58
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldInitializationStartable.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.unfold
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags
+import com.android.systemui.unfold.dagger.UnfoldBg
+import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder
+import java.util.Optional
+import javax.inject.Inject
+
+class UnfoldInitializationStartable
+@Inject
+constructor(
+ private val unfoldComponentOptional: Optional<SysUIUnfoldComponent>,
+ private val foldStateLoggingProviderOptional: Optional<FoldStateLoggingProvider>,
+ private val foldStateLoggerOptional: Optional<FoldStateLogger>,
+ @UnfoldBg
+ private val unfoldBgTransitionProgressProviderOptional:
+ Optional<UnfoldTransitionProgressProvider>,
+ private val unfoldTransitionProgressProviderOptional:
+ Optional<UnfoldTransitionProgressProvider>,
+ private val unfoldTransitionProgressForwarder: Optional<UnfoldTransitionProgressForwarder>
+) : CoreStartable {
+ override fun start() {
+ unfoldComponentOptional.ifPresent { c: SysUIUnfoldComponent ->
+ c.getFullScreenLightRevealAnimations().forEach { it: FullscreenLightRevealAnimation ->
+ it.init()
+ }
+ c.getUnfoldTransitionWallpaperController().init()
+ c.getUnfoldHapticsPlayer()
+ c.getNaturalRotationUnfoldProgressProvider().init()
+ c.getUnfoldLatencyTracker().init()
+ }
+
+ foldStateLoggingProviderOptional.ifPresent { obj: FoldStateLoggingProvider -> obj.init() }
+ foldStateLoggerOptional.ifPresent { obj: FoldStateLogger -> obj.init() }
+
+ val unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider> =
+ if (Flags.unfoldAnimationBackgroundProgress()) {
+ unfoldBgTransitionProgressProviderOptional
+ } else {
+ unfoldTransitionProgressProviderOptional
+ }
+ unfoldTransitionProgressProvider.ifPresent {
+ progressProvider: UnfoldTransitionProgressProvider ->
+ unfoldTransitionProgressForwarder.ifPresent {
+ listener: UnfoldTransitionProgressForwarder ->
+ progressProvider.addCallback(listener)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index b72c6f1..f355dd8 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -18,42 +18,22 @@
import android.annotation.BinderThread
import android.content.ContentResolver
import android.content.Context
-import android.graphics.PixelFormat
import android.hardware.devicestate.DeviceStateManager
-import android.hardware.devicestate.DeviceStateManager.FoldStateListener
-import android.hardware.display.DisplayManager
import android.hardware.input.InputManagerGlobal
import android.os.Handler
-import android.os.Looper
import android.os.Trace
-import android.view.Choreographer
-import android.view.Display
-import android.view.DisplayInfo
-import android.view.Surface
-import android.view.SurfaceControl
-import android.view.SurfaceControlViewHost
-import android.view.SurfaceSession
-import android.view.WindowManager
-import android.view.WindowlessWindowManager
-import com.android.app.tracing.traceSection
-import com.android.keyguard.logging.ScrimLogger
import com.android.systemui.Flags.unfoldAnimationBackgroundProgress
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
-import com.android.systemui.settings.DisplayTracker
-import com.android.systemui.statusbar.LightRevealEffect
-import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.LinearLightRevealEffect
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_OPAQUE
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.ALPHA_TRANSPARENT
+import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.FOLD
import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation.AddOverlayReason.UNFOLD
-import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.dagger.UnfoldBg
-import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
import com.android.systemui.util.concurrency.ThreadFactory
-import com.android.wm.shell.displayareahelper.DisplayAreaHelper
-import java.util.Optional
import java.util.concurrent.Executor
import java.util.function.Consumer
import javax.inject.Inject
@@ -64,80 +44,43 @@
@Inject
constructor(
private val context: Context,
- private val featureFlags: FeatureFlags,
- private val deviceStateManager: DeviceStateManager,
+ private val featureFlags: FeatureFlagsClassic,
private val contentResolver: ContentResolver,
- private val displayManager: DisplayManager,
+ @UnfoldBg private val unfoldProgressHandler: Handler,
@UnfoldBg
private val unfoldTransitionBgProgressProvider: Provider<UnfoldTransitionProgressProvider>,
private val unfoldTransitionProgressProvider: Provider<UnfoldTransitionProgressProvider>,
- private val displayAreaHelper: Optional<DisplayAreaHelper>,
- @Main private val executor: Executor,
+ private val deviceStateManager: DeviceStateManager,
private val threadFactory: ThreadFactory,
- @UnfoldBg private val rotationChangeProvider: RotationChangeProvider,
- @UnfoldBg private val unfoldProgressHandler: Handler,
- private val displayTracker: DisplayTracker,
- private val scrimLogger: ScrimLogger,
-) {
+ private val fullscreenLightRevealAnimationControllerFactory:
+ FullscreenLightRevealAnimationController.Factory
+) : FullscreenLightRevealAnimation {
private val transitionListener = TransitionListener()
- private val rotationWatcher = RotationWatcher()
-
- private lateinit var bgHandler: Handler
- private lateinit var bgExecutor: Executor
-
- private lateinit var wwm: WindowlessWindowManager
- private lateinit var unfoldedDisplayInfo: DisplayInfo
- private lateinit var overlayContainer: SurfaceControl
-
- private var root: SurfaceControlViewHost? = null
- private var scrimView: LightRevealScrim? = null
private var isFolded: Boolean = false
private var isUnfoldHandled: Boolean = true
- private var overlayAddReason: AddOverlayReason? = null
- private var isTouchBlocked: Boolean = true
+ private var overlayAddReason: AddOverlayReason = UNFOLD
+ private lateinit var controller: FullscreenLightRevealAnimationController
+ private lateinit var bgExecutor: Executor
- private var currentRotation: Int = context.display!!.rotation
-
- fun init() {
+ override fun init() {
// This method will be called only on devices where this animation is enabled,
// so normally this thread won't be created
- bgHandler = unfoldProgressHandler
- bgExecutor = threadFactory.buildDelayableExecutorOnHandler(bgHandler)
+ controller =
+ fullscreenLightRevealAnimationControllerFactory.create(
+ displaySelector = { maxByOrNull { it.naturalWidth } },
+ effectFactory = { LinearLightRevealEffect(it.isVerticalRotation()) },
+ overlayContainerName = SURFACE_CONTAINER_NAME,
+ )
+ controller.init()
+ bgExecutor = threadFactory.buildDelayableExecutorOnHandler(unfoldProgressHandler)
deviceStateManager.registerCallback(bgExecutor, FoldListener())
if (unfoldAnimationBackgroundProgress()) {
unfoldTransitionBgProgressProvider.get().addCallback(transitionListener)
} else {
unfoldTransitionProgressProvider.get().addCallback(transitionListener)
}
- rotationChangeProvider.addCallback(rotationWatcher)
-
- val containerBuilder =
- SurfaceControl.Builder(SurfaceSession())
- .setContainerLayer()
- .setName("unfold-overlay-container")
-
- displayAreaHelper.get().attachToRootDisplayArea(
- displayTracker.defaultDisplayId,
- containerBuilder
- ) { builder ->
- executor.execute {
- overlayContainer = builder.build()
-
- SurfaceControl.Transaction()
- .setLayer(overlayContainer, UNFOLD_OVERLAY_LAYER_Z_INDEX)
- .show(overlayContainer)
- .apply()
-
- wwm =
- WindowlessWindowManager(context.resources.configuration, overlayContainer, null)
- }
- }
-
- // Get unfolded display size immediately as 'current display info' might be
- // not up-to-date during unfolding
- unfoldedDisplayInfo = getUnfoldedDisplayInfo()
}
/**
@@ -148,17 +91,18 @@
* @see [com.android.systemui.keyguard.KeyguardViewMediator]
*/
@BinderThread
- fun onScreenTurningOn(onOverlayReady: Runnable) {
+ override fun onScreenTurningOn(onOverlayReady: Runnable) {
executeInBackground {
Trace.beginSection("$TAG#onScreenTurningOn")
try {
// Add the view only if we are unfolding and this is the first screen on
if (!isFolded && !isUnfoldHandled && contentResolver.areAnimationsEnabled()) {
- addOverlay(onOverlayReady, reason = UNFOLD)
+ overlayAddReason = UNFOLD
+ controller.addOverlay(calculateRevealAmount(), onOverlayReady)
isUnfoldHandled = true
} else {
// No unfold transition, immediately report that overlay is ready
- ensureOverlayRemoved()
+ controller.ensureOverlayRemoved()
onOverlayReady.run()
}
} finally {
@@ -167,78 +111,15 @@
}
}
- private fun addOverlay(onOverlayReady: Runnable? = null, reason: AddOverlayReason) {
- if (!::wwm.isInitialized) {
- // Surface overlay is not created yet on the first SysUI launch
- onOverlayReady?.run()
- return
- }
-
- ensureInBackground()
- ensureOverlayRemoved()
-
- overlayAddReason = reason
-
- val newRoot =
- SurfaceControlViewHost(
- context,
- context.display,
- wwm,
- "UnfoldLightRevealOverlayAnimation"
- )
- val params = getLayoutParams()
- val newView =
- LightRevealScrim(
- context,
- attrs = null,
- initialWidth = params.width,
- initialHeight = params.height
- )
- .apply {
- revealEffect = createLightRevealEffect()
- revealAmount = calculateRevealAmount()
- scrimLogger = this@UnfoldLightRevealOverlayAnimation.scrimLogger
- }
-
- newRoot.setView(newView, params)
-
- if (onOverlayReady != null) {
- Trace.beginAsyncSection("$TAG#relayout", 0)
-
- newRoot.relayout(params) { transaction ->
- val vsyncId = Choreographer.getSfInstance().vsyncId
-
- // Apply the transaction that contains the first frame of the overlay and apply
- // another empty transaction with 'vsyncId + 1' to make sure that it is actually
- // displayed on the screen. The second transaction is necessary to remove the screen
- // blocker (turn on the brightness) only when the content is actually visible as it
- // might be presented only in the next frame.
- // See b/197538198
- transaction.setFrameTimelineVsync(vsyncId).apply()
-
- transaction
- .setFrameTimelineVsync(vsyncId + 1)
- .addTransactionCommittedListener(bgExecutor) {
- Trace.endAsyncSection("$TAG#relayout", 0)
- onOverlayReady.run()
- }
- .apply()
- }
- }
-
- scrimView = newView
- root = newRoot
- }
-
private fun calculateRevealAmount(animationProgress: Float? = null): Float {
- val overlayAddReason = overlayAddReason ?: UNFOLD
+ val overlayAddReason = overlayAddReason
if (animationProgress == null) {
- // Animation progress is unknown, calculate the initial value based on the overlay
+ // Animation progress unknown, calculate the initial value based on the overlay
// add reason
return when (overlayAddReason) {
- FOLD -> TRANSPARENT
- UNFOLD -> BLACK
+ FOLD -> ALPHA_TRANSPARENT
+ UNFOLD -> ALPHA_OPAQUE
}
}
@@ -249,144 +130,57 @@
// Do not darken the content when SHOW_VIGNETTE_WHEN_FOLDING flag is off
// and we are folding the device. We still add the overlay to block touches
// while the animation is running but the overlay is transparent.
- TRANSPARENT
+ ALPHA_TRANSPARENT
} else {
animationProgress
}
}
- private fun getLayoutParams(): WindowManager.LayoutParams {
- val params: WindowManager.LayoutParams = WindowManager.LayoutParams()
+ private inner class TransitionListener :
+ UnfoldTransitionProgressProvider.TransitionProgressListener {
- val rotation = currentRotation
- val isNatural = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
-
- params.height =
- if (isNatural) unfoldedDisplayInfo.naturalHeight else unfoldedDisplayInfo.naturalWidth
- params.width =
- if (isNatural) unfoldedDisplayInfo.naturalWidth else unfoldedDisplayInfo.naturalHeight
-
- params.format = PixelFormat.TRANSLUCENT
- params.type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
- params.title = "Unfold Light Reveal Animation"
- params.layoutInDisplayCutoutMode =
- WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
- params.fitInsetsTypes = 0
-
- val touchFlags =
- if (isTouchBlocked) {
- // Touchable by default, so it will block the touches
- 0
- } else {
- WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
- }
- params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or touchFlags
- params.setTrustedOverlay()
-
- val packageName: String = context.opPackageName
- params.packageName = packageName
-
- return params
- }
-
- private fun updateTouchBlockIfNeeded(progress: Float) {
- // When unfolding unblock touches a bit earlier than the animation end as the
- // interpolation has a long tail of very slight movement at the end which should not
- // affect much the usage of the device
- val shouldBlockTouches =
- if (overlayAddReason == UNFOLD) {
- progress < UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS
- } else {
- true
- }
-
- if (isTouchBlocked != shouldBlockTouches) {
- isTouchBlocked = shouldBlockTouches
-
- traceSection("$TAG#relayoutToUpdateTouch") { root?.relayout(getLayoutParams()) }
- }
- }
-
- private fun createLightRevealEffect(): LightRevealEffect {
- val isVerticalFold =
- currentRotation == Surface.ROTATION_0 || currentRotation == Surface.ROTATION_180
- return LinearLightRevealEffect(isVertical = isVerticalFold)
- }
-
- private fun ensureOverlayRemoved() {
- ensureInBackground()
- traceSection("ensureOverlayRemoved") {
- root?.release()
- root = null
- scrimView = null
- }
- }
-
- private fun getUnfoldedDisplayInfo(): DisplayInfo =
- displayManager
- .getDisplays(DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)
- .asSequence()
- .map { DisplayInfo().apply { it.getDisplayInfo(this) } }
- .filter { it.type == Display.TYPE_INTERNAL }
- .maxByOrNull { it.naturalWidth }!!
-
- private inner class TransitionListener : TransitionProgressListener {
-
- override fun onTransitionProgress(progress: Float) {
- executeInBackground {
- scrimView?.revealAmount = calculateRevealAmount(progress)
- updateTouchBlockIfNeeded(progress)
- }
+ override fun onTransitionProgress(progress: Float) = executeInBackground {
+ controller.updateRevealAmount(calculateRevealAmount(progress))
+ // When unfolding unblock touches a bit earlier than the animation end as the
+ // interpolation has a long tail of very slight movement at the end which should not
+ // affect much the usage of the device
+ controller.isTouchBlocked =
+ overlayAddReason == FOLD || progress < UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS
}
- override fun onTransitionFinished() {
- executeInBackground { ensureOverlayRemoved() }
+ override fun onTransitionFinished() = executeInBackground {
+ controller.ensureOverlayRemoved()
}
override fun onTransitionStarted() {
// Add view for folding case (when unfolding the view is added earlier)
- if (scrimView == null) {
- executeInBackground { addOverlay(reason = FOLD) }
+ if (controller.isOverlayVisible()) {
+ executeInBackground {
+ overlayAddReason = FOLD
+ controller.addOverlay(calculateRevealAmount())
+ }
}
// Disable input dispatching during transition.
InputManagerGlobal.getInstance().cancelCurrentTouch()
}
}
- private inner class RotationWatcher : RotationChangeProvider.RotationListener {
- override fun onRotationChanged(newRotation: Int) {
- executeInBackground {
- traceSection("$TAG#onRotationChanged") {
- if (currentRotation != newRotation) {
- currentRotation = newRotation
- scrimView?.revealEffect = createLightRevealEffect()
- root?.relayout(getLayoutParams())
- }
- }
- }
- }
- }
-
private fun executeInBackground(f: () -> Unit) {
// This is needed to allow progresses to be received both from the main thread (that will
// schedule a runnable on the bg thread), and from the bg thread directly (no reposting).
- if (bgHandler.looper.isCurrentThread) {
+ if (unfoldProgressHandler.looper.isCurrentThread) {
f()
} else {
- bgHandler.post(f)
+ unfoldProgressHandler.post(f)
}
}
- private fun ensureInBackground() {
- check(Looper.myLooper() == bgHandler.looper) { "Not being executed in the background!" }
- }
-
private inner class FoldListener :
- FoldStateListener(
+ DeviceStateManager.FoldStateListener(
context,
Consumer { isFolded ->
if (isFolded) {
- ensureOverlayRemoved()
+ controller.ensureOverlayRemoved()
isUnfoldHandled = false
}
this.isFolded = isFolded
@@ -400,16 +194,7 @@
private companion object {
const val TAG = "UnfoldLightRevealOverlayAnimation"
- const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE
-
- // Put the unfold overlay below the rotation animation screenshot to hide the moment
- // when it is rotated but the rotation of the other windows hasn't happen yet
- const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1
-
- // constants for revealAmount.
- const val TRANSPARENT = 1f
- const val BLACK = 0f
-
- private const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f
+ const val SURFACE_CONTAINER_NAME = "unfold-overlay-container"
+ const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
index c170eb5..a122311 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
@@ -27,7 +27,6 @@
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
-import android.os.Process
import android.os.RemoteException
import android.os.UserHandle
import android.os.UserManager
@@ -50,6 +49,7 @@
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.process.ProcessWrapper
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.res.R
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -108,6 +108,7 @@
private val guestUserInteractor: GuestUserInteractor,
private val uiEventLogger: UiEventLogger,
private val userRestrictionChecker: UserRestrictionChecker,
+ private val processWrapper: ProcessWrapper
) {
/**
* Defines interface for classes that can be notified when the state of users on the device is
@@ -669,7 +670,7 @@
// Connect to the new secondary user's service (purely to ensure that a persistent
// SystemUI application is created for that user)
- if (userId != Process.myUserHandle().identifier) {
+ if (userId != processWrapper.myUserHandle().identifier && !processWrapper.isSystemUser) {
applicationContext.startServiceAsUser(
intent,
UserHandle.of(userId),
diff --git a/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt b/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt
deleted file mode 100644
index d3653b4..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt
+++ /dev/null
@@ -1,35 +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.util.view
-
-import android.view.View
-import com.android.systemui.util.kotlin.awaitCancellationThenDispose
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collectLatest
-
-/**
- * Use the [bind] method to bind the view every time this flow emits, and suspend to await for more
- * updates. New emissions lead to the previous binding call being cancelled if not completed.
- * Dispose of the [DisposableHandle] returned by [bind] when done.
- */
-suspend fun <T : View> Flow<T>.bindLatest(bind: (T) -> DisposableHandle?) {
- this.collectLatest { view ->
- val disposableHandle = bind(view)
- disposableHandle?.awaitCancellationThenDispose()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index ce6d740..90c5c62 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -116,9 +116,8 @@
import com.android.settingslib.Utils;
import com.android.systemui.Dumpable;
import com.android.systemui.Prefs;
-import com.android.systemui.dagger.qualifiers.Application;
-import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.haptics.slider.HapticSliderViewBinder;
import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
@@ -145,9 +144,6 @@
import java.util.List;
import java.util.function.Consumer;
-import kotlinx.coroutines.CoroutineDispatcher;
-import kotlinx.coroutines.CoroutineScope;
-
/**
* Visual presentation of the volume dialog.
*
@@ -311,8 +307,6 @@
private int mOrientation;
private final Lazy<SecureSettings> mSecureSettings;
private int mDialogTimeoutMillis;
- private final CoroutineDispatcher mMainDispatcher;
- private final CoroutineScope mApplicationScope;
private final VibratorHelper mVibratorHelper;
private final com.android.systemui.util.time.SystemClock mSystemClock;
@@ -333,14 +327,10 @@
DumpManager dumpManager,
Lazy<SecureSettings> secureSettings,
VibratorHelper vibratorHelper,
- @Main CoroutineDispatcher mainDispatcher,
- @Application CoroutineScope applicationScope,
com.android.systemui.util.time.SystemClock systemClock) {
mContext =
new ContextThemeWrapper(context, R.style.volume_dialog_theme);
mHandler = new H(looper);
- mMainDispatcher = mainDispatcher;
- mApplicationScope = applicationScope;
mVibratorHelper = vibratorHelper;
mSystemClock = systemClock;
mShouldListenForJank = shouldListenForJank;
@@ -858,7 +848,10 @@
row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)});
}
row.slider = row.view.findViewById(R.id.volume_row_slider);
- row.createPlugin(mVibratorHelper, mSystemClock, mMainDispatcher, mApplicationScope);
+ if (hapticVolumeSlider()) {
+ row.createPlugin(mVibratorHelper, mSystemClock);
+ HapticSliderViewBinder.bind(row.slider, row.mHapticPlugin);
+ }
row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row));
row.number = row.view.findViewById(R.id.volume_number);
@@ -1498,7 +1491,7 @@
for (int i = 0; i < mRows.size(); i++) {
VolumeRow row = mRows.get(i);
if (row.slider.getVisibility() == VISIBLE) {
- row.addHaptics();
+ row.addTouchListener();
}
}
Trace.endSection();
@@ -2620,17 +2613,13 @@
void createPlugin(
VibratorHelper vibratorHelper,
- com.android.systemui.util.time.SystemClock systemClock,
- CoroutineDispatcher mainDispatcher,
- CoroutineScope applicationScope) {
- if (!hapticVolumeSlider() || mHapticPlugin != null) return;
+ com.android.systemui.util.time.SystemClock systemClock) {
+ if (mHapticPlugin != null) return;
mHapticPlugin = new SeekableSliderHapticPlugin(
- vibratorHelper,
- systemClock,
- mainDispatcher,
- applicationScope,
- sSliderHapticFeedbackConfig);
+ vibratorHelper,
+ systemClock,
+ sSliderHapticFeedbackConfig);
}
@@ -2647,19 +2636,9 @@
});
}
- void addHaptics() {
- if (mHapticPlugin != null) {
- addTouchListener();
- mHapticPlugin.start();
- }
- }
-
@SuppressLint("ClickableViewAccessibility")
void removeHaptics() {
slider.setOnTouchListener(null);
- if (mHapticPlugin != null) {
- mHapticPlugin.stop();
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index ff1daea..278ffc6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -21,6 +21,8 @@
import com.android.settingslib.volume.data.repository.AudioRepository
import com.android.settingslib.volume.data.repository.AudioRepositoryImpl
import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiverImpl
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import dagger.Module
@@ -35,13 +37,19 @@
companion object {
@Provides
- fun provideAudioRepository(
+ fun provideAudioManagerIntentsReceiver(
@Application context: Context,
+ @Application coroutineScope: CoroutineScope,
+ ): AudioManagerIntentsReceiver = AudioManagerIntentsReceiverImpl(context, coroutineScope)
+
+ @Provides
+ fun provideAudioRepository(
+ intentsReceiver: AudioManagerIntentsReceiver,
audioManager: AudioManager,
@Background coroutineContext: CoroutineContext,
@Application coroutineScope: CoroutineScope,
): AudioRepository =
- AudioRepositoryImpl(context, audioManager, coroutineContext, coroutineScope)
+ AudioRepositoryImpl(intentsReceiver, audioManager, coroutineContext, coroutineScope)
@Provides
fun provideAudioModeInteractor(repository: AudioRepository): AudioModeInteractor =
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
index 2ff9af9..ab76d45 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
@@ -16,11 +16,11 @@
package com.android.systemui.volume.dagger
-import android.content.Context
import android.media.session.MediaSessionManager
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.volume.data.repository.MediaControllerRepository
import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
@@ -37,14 +37,14 @@
@Provides
@SysUISingleton
fun provideMediaDeviceSessionRepository(
- @Application context: Context,
+ intentsReceiver: AudioManagerIntentsReceiver,
mediaSessionManager: MediaSessionManager,
localBluetoothManager: LocalBluetoothManager?,
@Application coroutineScope: CoroutineScope,
@Background backgroundContext: CoroutineContext,
): MediaControllerRepository =
MediaControllerRepositoryImpl(
- context,
+ intentsReceiver,
mediaSessionManager,
localBluetoothManager,
coroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index 2718839..3285637 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -23,8 +23,6 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.systemui.CoreStartable;
-import com.android.systemui.dagger.qualifiers.Application;
-import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.VolumeDialog;
@@ -55,9 +53,6 @@
import dagger.multibindings.IntoMap;
import dagger.multibindings.IntoSet;
-import kotlinx.coroutines.CoroutineDispatcher;
-import kotlinx.coroutines.CoroutineScope;
-
/** Dagger Module for code in the volume package. */
@Module(
includes = {
@@ -112,8 +107,6 @@
DumpManager dumpManager,
Lazy<SecureSettings> secureSettings,
VibratorHelper vibratorHelper,
- @Main CoroutineDispatcher mainDispatcher,
- @Application CoroutineScope applicationScope,
SystemClock systemClock) {
VolumeDialogImpl impl = new VolumeDialogImpl(
context,
@@ -132,8 +125,6 @@
dumpManager,
secureSettings,
vibratorHelper,
- mainDispatcher,
- applicationScope,
systemClock);
impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
impl.setAutomute(true);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
index 57ac435..0a1ee24 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
@@ -15,8 +15,10 @@
*/
package com.android.systemui.volume.panel.component.mediaoutput.data.repository
+import android.media.MediaRouter2Manager
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl
+import com.android.settingslib.volume.shared.AudioManagerIntentsReceiver
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.media.controls.pipeline.LocalMediaManagerFactory
@@ -27,6 +29,8 @@
class LocalMediaRepositoryFactory
@Inject
constructor(
+ private val intentsReceiver: AudioManagerIntentsReceiver,
+ private val mediaRouter2Manager: MediaRouter2Manager,
private val localMediaManagerFactory: LocalMediaManagerFactory,
@Application private val coroutineScope: CoroutineScope,
@Background private val backgroundCoroutineContext: CoroutineContext,
@@ -34,7 +38,9 @@
fun create(packageName: String?): LocalMediaRepository =
LocalMediaRepositoryImpl(
+ intentsReceiver,
localMediaManagerFactory.create(packageName),
+ mediaRouter2Manager,
coroutineScope,
backgroundCoroutineContext,
)
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index c2efc05..d048cbe 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -28,6 +28,7 @@
import com.android.internal.widget.LockscreenCredential
import com.android.keyguard.KeyguardPinViewController.PinBouncerUiEvent
import com.android.keyguard.KeyguardSecurityModel.SecurityMode
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.classifier.FalsingCollectorFake
@@ -88,6 +89,7 @@
@Mock private val mEmergencyButtonController: EmergencyButtonController? = null
private val falsingCollector: FalsingCollector = FalsingCollectorFake()
+ private val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository())
@Mock lateinit var postureController: DevicePostureController
@Mock lateinit var mSelectedUserInteractor: SelectedUserInteractor
@@ -143,7 +145,7 @@
featureFlags,
mSelectedUserInteractor,
uiEventLogger,
- FakeKeyboardRepository()
+ keyguardKeyboardInteractor
)
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
index 0959f1b..4a2554e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
@@ -23,6 +23,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.internal.widget.LockPatternUtils
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
@@ -80,6 +81,7 @@
LayoutInflater.from(context).inflate(R.layout.keyguard_sim_pin_view, null)
as KeyguardSimPinView
val fakeFeatureFlags = FakeFeatureFlags()
+ val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository())
mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES)
underTest =
@@ -97,7 +99,7 @@
emergencyButtonController,
fakeFeatureFlags,
mSelectedUserInteractor,
- FakeKeyboardRepository()
+ keyguardKeyboardInteractor
)
underTest.init()
underTest.onViewAttached()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
index 1281e44..4f46184 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
@@ -24,6 +24,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.internal.widget.LockPatternUtils
+import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
@@ -75,6 +76,7 @@
simPukView =
LayoutInflater.from(context).inflate(R.layout.keyguard_sim_puk_view, null)
as KeyguardSimPukView
+ val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository())
val fakeFeatureFlags = FakeFeatureFlags()
mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES)
underTest =
@@ -92,7 +94,7 @@
emergencyButtonController,
fakeFeatureFlags,
mSelectedUserInteractor,
- FakeKeyboardRepository()
+ keyguardKeyboardInteractor
)
underTest.init()
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
index b45c894..fb649c8 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt
@@ -24,8 +24,8 @@
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.FoldAodAnimationController
+import com.android.systemui.unfold.FullscreenLightRevealAnimation
import com.android.systemui.unfold.SysUIUnfoldComponent
-import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation
import com.android.systemui.util.mockito.capture
import com.android.systemui.utils.os.FakeHandler
import com.android.systemui.utils.os.FakeHandler.Mode.QUEUEING
@@ -53,7 +53,9 @@
@Mock
private lateinit var foldAodAnimationController: FoldAodAnimationController
@Mock
- private lateinit var unfoldAnimation: UnfoldLightRevealOverlayAnimation
+ private lateinit var fullscreenLightRevealAnimation: FullscreenLightRevealAnimation
+ @Mock
+ private lateinit var fullScreenLightRevealAnimations: Set<FullscreenLightRevealAnimation>
@Captor
private lateinit var readyCaptor: ArgumentCaptor<Runnable>
@@ -67,9 +69,9 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
-
- `when`(unfoldComponent.getUnfoldLightRevealOverlayAnimation())
- .thenReturn(unfoldAnimation)
+ fullScreenLightRevealAnimations = setOf(fullscreenLightRevealAnimation)
+ `when`(unfoldComponent.getFullScreenLightRevealAnimations())
+ .thenReturn(fullScreenLightRevealAnimations)
`when`(unfoldComponent.getFoldAodAnimationController())
.thenReturn(foldAodAnimationController)
@@ -164,7 +166,7 @@
}
private fun onUnfoldOverlayReady() {
- verify(unfoldAnimation).onScreenTurningOn(capture(readyCaptor))
+ verify(fullscreenLightRevealAnimation).onScreenTurningOn(capture(readyCaptor))
readyCaptor.value.run()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
index 202d9ce..e157fc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
@@ -91,7 +91,7 @@
whenever(sysuiComponent.startables)
.thenReturn(mutableMapOf(StartableA::class.java to Provider { startableA }))
app.onCreate()
- app.startServicesIfNeeded()
+ app.startSystemUserServicesIfNeeded()
assertThat(startableA.started).isTrue()
}
@@ -105,7 +105,7 @@
)
)
app.onCreate()
- app.startServicesIfNeeded()
+ app.startSystemUserServicesIfNeeded()
assertThat(startableA.started).isTrue()
assertThat(startableB.started).isTrue()
}
@@ -121,7 +121,7 @@
)
)
app.onCreate()
- app.startServicesIfNeeded()
+ app.startSystemUserServicesIfNeeded()
assertThat(startableA.started).isTrue()
assertThat(startableB.started).isTrue()
assertThat(startableC.started).isTrue()
@@ -141,7 +141,7 @@
)
)
app.onCreate()
- app.startServicesIfNeeded()
+ app.startSystemUserServicesIfNeeded()
assertThat(startableA.started).isTrue()
assertThat(startableB.started).isTrue()
assertThat(startableC.started).isTrue()
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 375ebe8..8299acb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -51,7 +51,6 @@
import android.view.accessibility.IRemoteMagnificationAnimationCallback;
import android.view.animation.AccelerateInterpolator;
-import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
@@ -80,7 +79,6 @@
@LargeTest
@RunWith(AndroidTestingRunner.class)
-@FlakyTest(bugId = 308501761)
public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
@Rule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
index 8faf715..722107c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
@@ -45,11 +45,11 @@
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class ActivityLaunchAnimatorTest : SysuiTestCase() {
- private val launchContainer = LinearLayout(mContext)
- private val testLaunchAnimator = fakeLaunchAnimator()
+ private val transitionContainer = LinearLayout(mContext)
+ private val testTransitionAnimator = fakeTransitionAnimator()
@Mock lateinit var callback: ActivityLaunchAnimator.Callback
@Mock lateinit var listener: ActivityLaunchAnimator.Listener
- @Spy private val controller = TestLaunchAnimatorController(launchContainer)
+ @Spy private val controller = TestLaunchAnimatorController(transitionContainer)
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
@@ -58,7 +58,11 @@
@Before
fun setup() {
activityLaunchAnimator =
- ActivityLaunchAnimator(testLaunchAnimator, testLaunchAnimator, disableWmTimeout = true)
+ ActivityLaunchAnimator(
+ testTransitionAnimator,
+ testTransitionAnimator,
+ disableWmTimeout = true
+ )
activityLaunchAnimator.callback = callback
activityLaunchAnimator.addListener(listener)
}
@@ -165,7 +169,7 @@
waitForIdleSync()
verify(controller).onLaunchAnimationCancelled()
- verify(controller, never()).onLaunchAnimationStart(anyBoolean())
+ verify(controller, never()).onTransitionAnimationStart(anyBoolean())
verify(listener).onLaunchAnimationCancelled()
verify(listener, never()).onLaunchAnimationStart()
assertNull(runner.delegate)
@@ -178,7 +182,7 @@
waitForIdleSync()
verify(controller).onLaunchAnimationCancelled()
- verify(controller, never()).onLaunchAnimationStart(anyBoolean())
+ verify(controller, never()).onTransitionAnimationStart(anyBoolean())
verify(listener).onLaunchAnimationCancelled()
verify(listener, never()).onLaunchAnimationStart()
assertNull(runner.delegate)
@@ -190,7 +194,7 @@
runner.onAnimationStart(0, arrayOf(fakeWindow()), emptyArray(), emptyArray(), iCallback)
waitForIdleSync()
verify(listener).onLaunchAnimationStart()
- verify(controller).onLaunchAnimationStart(anyBoolean())
+ verify(controller).onTransitionAnimationStart(anyBoolean())
}
@Test
@@ -240,10 +244,10 @@
* A simple implementation of [ActivityLaunchAnimator.Controller] which throws if it is called
* outside of the main thread.
*/
-private class TestLaunchAnimatorController(override var launchContainer: ViewGroup) :
+private class TestLaunchAnimatorController(override var transitionContainer: ViewGroup) :
ActivityLaunchAnimator.Controller {
override fun createAnimatorState() =
- LaunchAnimator.State(
+ TransitionAnimator.State(
top = 100,
bottom = 200,
left = 300,
@@ -262,19 +266,19 @@
assertOnMainThread()
}
- override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
assertOnMainThread()
}
- override fun onLaunchAnimationProgress(
- state: LaunchAnimator.State,
+ override fun onTransitionAnimationProgress(
+ state: TransitionAnimator.State,
progress: Float,
linearProgress: Float
) {
assertOnMainThread()
}
- override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+ override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
assertOnMainThread()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
index 2233e322..a586421 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
@@ -129,14 +129,14 @@
// The dialog shouldn't be dismissable during the animation.
runOnMainThreadAndWaitForIdleSync {
- controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationStart(isExpandingFullyAbove = true)
secondDialog.dismiss()
}
assertTrue(secondDialog.isShowing)
// Both dialogs should be dismissed at the end of the animation.
runOnMainThreadAndWaitForIdleSync {
- controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove = true)
}
assertFalse(firstDialog.isShowing)
assertFalse(secondDialog.isShowing)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
index d1ac0e8..8442a62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewLaunchAnimatorControllerTest.kt
@@ -32,13 +32,13 @@
class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() {
@Test
fun animatingOrphanViewDoesNotCrash() {
- val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
+ val state = TransitionAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
val controller = GhostedViewLaunchAnimatorController(LaunchableFrameLayout(mContext))
controller.onIntentStarted(willAnimate = true)
- controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
- controller.onLaunchAnimationProgress(state, progress = 0f, linearProgress = 0f)
- controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationStart(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationProgress(state, progress = 0f, linearProgress = 0f)
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove = true)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt
deleted file mode 100644
index 112cec2..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt
+++ /dev/null
@@ -1,170 +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.common.ui
-
-import android.content.Context
-import android.testing.AndroidTestingRunner
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.mockito.captureMany
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.cancelAndJoin
-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.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.atLeastOnce
-import org.mockito.Mockito.verify
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class ConfigurationStateTest : SysuiTestCase() {
-
- private val configurationController: ConfigurationController = mock()
- private val layoutInflater = TestLayoutInflater()
- private val backgroundDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(backgroundDispatcher)
-
- val underTest = ConfigurationState(configurationController, context, layoutInflater)
-
- @Test
- fun reinflateAndBindLatest_inflatesWithoutEmission() =
- testScope.runTest {
- var callbackCount = 0
- backgroundScope.launch {
- underTest.reinflateAndBindLatest<View>(
- resource = 0,
- root = null,
- attachToRoot = false,
- backgroundDispatcher,
- ) {
- callbackCount++
- null
- }
- }
-
- // Inflates without an emission
- runCurrent()
- assertThat(layoutInflater.inflationCount).isEqualTo(1)
- assertThat(callbackCount).isEqualTo(1)
- }
-
- @Test
- fun reinflateAndBindLatest_reinflatesOnThemeChanged() =
- testScope.runTest {
- var callbackCount = 0
- backgroundScope.launch {
- underTest.reinflateAndBindLatest<View>(
- resource = 0,
- root = null,
- attachToRoot = false,
- backgroundDispatcher,
- ) {
- callbackCount++
- null
- }
- }
- runCurrent()
-
- val configListeners: List<ConfigurationController.ConfigurationListener> = captureMany {
- verify(configurationController, atLeastOnce()).addCallback(capture())
- }
-
- listOf(1, 2, 3).forEach { count ->
- assertThat(layoutInflater.inflationCount).isEqualTo(count)
- assertThat(callbackCount).isEqualTo(count)
- configListeners.forEach { it.onThemeChanged() }
- runCurrent()
- }
- }
-
- @Test
- fun reinflateAndBindLatest_reinflatesOnDensityOrFontScaleChanged() =
- testScope.runTest {
- var callbackCount = 0
- backgroundScope.launch {
- underTest.reinflateAndBindLatest<View>(
- resource = 0,
- root = null,
- attachToRoot = false,
- backgroundDispatcher,
- ) {
- callbackCount++
- null
- }
- }
- runCurrent()
-
- val configListeners: List<ConfigurationController.ConfigurationListener> = captureMany {
- verify(configurationController, atLeastOnce()).addCallback(capture())
- }
-
- listOf(1, 2, 3).forEach { count ->
- assertThat(layoutInflater.inflationCount).isEqualTo(count)
- assertThat(callbackCount).isEqualTo(count)
- configListeners.forEach { it.onDensityOrFontScaleChanged() }
- runCurrent()
- }
- }
-
- @Test
- fun testReinflateAndBindLatest_disposesOnCancel() =
- testScope.runTest {
- var callbackCount = 0
- var disposed = false
- val job = launch {
- underTest.reinflateAndBindLatest<View>(
- resource = 0,
- root = null,
- attachToRoot = false,
- backgroundDispatcher,
- ) {
- callbackCount++
- DisposableHandle { disposed = true }
- }
- }
-
- runCurrent()
- job.cancelAndJoin()
- assertThat(disposed).isTrue()
- }
-
- inner class TestLayoutInflater : LayoutInflater(context) {
-
- var inflationCount = 0
-
- override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View {
- inflationCount++
- return View(context)
- }
-
- override fun cloneInContext(p0: Context?): LayoutInflater {
- // not needed for this test
- return this
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
index b28d0c8..e30dd35d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
@@ -33,8 +33,7 @@
import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfig
-import com.android.systemui.deviceentry.data.repository.faceWakeUpTriggersConfig
+import com.android.systemui.deviceentry.data.repository.fakeFaceWakeUpTriggersConfig
import com.android.systemui.deviceentry.shared.FaceAuthUiEvent
import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
@@ -56,10 +55,9 @@
import com.android.systemui.user.data.model.SelectionStatus
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -68,37 +66,33 @@
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() {
- val kosmos =
- testKosmos().apply { this.faceWakeUpTriggersConfig = mock<FaceWakeUpTriggersConfig>() }
+ private val kosmos = testKosmos()
+ private val testScope: TestScope = kosmos.testScope
private lateinit var underTest: SystemUIDeviceEntryFaceAuthInteractor
- private val testScope = kosmos.testScope
private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
private val fakeUserRepository = kosmos.fakeUserRepository
private val facePropertyRepository = kosmos.facePropertyRepository
- private val fakeDeviceEntryFingerprintAuthRepository =
- kosmos.fakeDeviceEntryFingerprintAuthRepository
+ private val fakeDeviceEntryFingerprintAuthInteractor =
+ kosmos.deviceEntryFingerprintAuthInteractor
private val powerInteractor = kosmos.powerInteractor
private val fakeBiometricSettingsRepository = kosmos.fakeBiometricSettingsRepository
private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor
- private val faceWakeUpTriggersConfig = kosmos.faceWakeUpTriggersConfig
+ private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig
private val trustManager = kosmos.trustManager
@Before
fun setup() {
- MockitoAnnotations.initMocks(this)
fakeUserRepository.setUserInfos(listOf(primaryUser, secondaryUser))
-
underTest =
SystemUIDeviceEntryFaceAuthInteractor(
mContext,
@@ -110,7 +104,7 @@
keyguardTransitionInteractor,
FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")),
keyguardUpdateMonitor,
- fakeDeviceEntryFingerprintAuthRepository,
+ fakeDeviceEntryFingerprintAuthInteractor,
fakeUserRepository,
facePropertyRepository,
faceWakeUpTriggersConfig,
@@ -126,10 +120,9 @@
underTest.start()
powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID)
- whenever(
- faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LID)
- )
- .thenReturn(true)
+ faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom(
+ setOf(WakeSleepReason.LID.powerManagerWakeReason)
+ )
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
@@ -168,10 +161,9 @@
underTest.start()
powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID)
- whenever(
- faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LID)
- )
- .thenReturn(true)
+ faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom(
+ setOf(WakeSleepReason.LID.powerManagerWakeReason)
+ )
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
@@ -194,10 +186,9 @@
underTest.start()
powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LIFT)
- whenever(
- faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LIFT)
- )
- .thenReturn(false)
+ faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom(
+ setOf(WakeSleepReason.LID.powerManagerWakeReason)
+ )
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
@@ -217,10 +208,9 @@
underTest.start()
powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID)
- whenever(
- faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(WakeSleepReason.LID)
- )
- .thenReturn(true)
+ faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom(
+ setOf(WakeSleepReason.LID.powerManagerWakeReason)
+ )
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
@@ -440,7 +430,45 @@
underTest.start()
fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
- fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(faceAuthRepository.isLockedOut.value).isTrue()
+ }
+
+ @Test
+ fun faceLockoutStateIsResetWheneverFingerprintIsNotLockedOut() =
+ testScope.runTest {
+ underTest.start()
+ fakeUserRepository.setSelectedUserInfo(primaryUser, SelectionStatus.SELECTION_COMPLETE)
+ fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(faceAuthRepository.isLockedOut.value).isTrue()
+ facePropertyRepository.setLockoutMode(primaryUserId, LockoutMode.NONE)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ runCurrent()
+
+ assertThat(faceAuthRepository.isLockedOut.value).isFalse()
+ }
+
+ @Test
+ fun faceLockoutStateIsSetToUsersLockoutStateWheneverFingerprintIsNotLockedOut() =
+ testScope.runTest {
+ underTest.start()
+ fakeUserRepository.setSelectedUserInfo(primaryUser, SelectionStatus.SELECTION_COMPLETE)
+ fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(faceAuthRepository.isLockedOut.value).isTrue()
+ facePropertyRepository.setLockoutMode(primaryUserId, LockoutMode.TIMED)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
runCurrent()
assertThat(faceAuthRepository.isLockedOut.value).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
index db04962..796d6d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
@@ -20,8 +20,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@@ -36,6 +36,7 @@
import org.mockito.MockitoAnnotations
@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class SeekableSliderTrackerTest : SysuiTestCase() {
@@ -51,7 +52,7 @@
@Test
fun initializeSliderTracker_startsTracking() = runTest {
// GIVEN Initialized tracker
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// THEN the tracker job is active
assertThat(mSeekableSliderTracker.isTracking).isTrue()
@@ -61,7 +62,7 @@
fun stopTracking_onAnyState_resetsToIdle() = runTest {
enumValues<SliderState>().forEach {
// GIVEN Initialized tracker
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a state in the state machine
mSeekableSliderTracker.setState(it)
@@ -79,7 +80,7 @@
@Test
fun initializeSliderTracker_isIdle() = runTest {
// GIVEN Initialized tracker
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// THEN The state is idle and the listener is not called to play haptics
assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
@@ -88,7 +89,7 @@
@Test
fun startsTrackingTouch_onIdle_entersWaitState() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a start of tracking touch event
val progress = 0f
@@ -106,7 +107,7 @@
@Test
fun waitCompletes_onWait_movesToHandleAcquired() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT
val progress = 0f
@@ -126,7 +127,7 @@
@Test
fun impreciseTouch_onWait_movesToHandleAcquired() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
@@ -151,7 +152,7 @@
@Test
fun trackJump_onWait_movesToJumpTrackLocationSelected() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
@@ -175,7 +176,7 @@
@Test
fun upperBookendSelection_onWait_movesToBookendSelected() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
@@ -197,7 +198,7 @@
@Test
fun lowerBookendSelection_onWait_movesToBookendSelected() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
@@ -219,7 +220,7 @@
@Test
fun stopTracking_onWait_whenWaitingJobIsActive_resetsToIdle() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
@@ -240,7 +241,7 @@
@Test
fun progressChangeByUser_onJumpTrackLocationSelected_movesToDragHandleDragging() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_TRACK_LOCATION_SELECTED state
mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
@@ -256,7 +257,7 @@
@Test
fun touchRelease_onJumpTrackLocationSelected_movesToIdle() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_TRACK_LOCATION_SELECTED state
mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
@@ -272,7 +273,7 @@
@Test
fun progressChangeByUser_onJumpBookendSelected_movesToDragHandleDragging() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_BOOKEND_SELECTED state
mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
@@ -288,7 +289,7 @@
@Test
fun touchRelease_onJumpBookendSelected_movesToIdle() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_BOOKEND_SELECTED state
mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
@@ -306,7 +307,7 @@
@Test
fun progressChangeByUser_onHandleAcquired_movesToDragHandleDragging() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
@@ -325,7 +326,7 @@
@Test
fun touchRelease_onHandleAcquired_movesToIdle() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
@@ -344,7 +345,7 @@
@Test
fun progressChangeByUser_onHandleDragging_progressOutsideOfBookends_doesNotChangeState() =
runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_DRAGGING state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
@@ -366,7 +367,7 @@
fun progressChangeByUser_onHandleDragging_reachesLowerBookend_movesToHandleReachedBookend() =
runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_DRAGGING state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
@@ -389,7 +390,7 @@
fun progressChangeByUser_onHandleDragging_reachesUpperBookend_movesToHandleReachedBookend() =
runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_DRAGGING state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
@@ -410,7 +411,7 @@
@Test
fun touchRelease_onHandleDragging_movesToIdle() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_DRAGGING state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
@@ -430,7 +431,7 @@
fun progressChangeByUser_outsideOfBookendRange_onLowerBookend_movesToDragHandleDragging() =
runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
@@ -451,7 +452,7 @@
@Test
fun progressChangeByUser_insideOfBookendRange_onLowerBookend_doesNotChangeState() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
@@ -473,7 +474,7 @@
fun progressChangeByUser_outsideOfBookendRange_onUpperBookend_movesToDragHandleDragging() =
runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
@@ -494,7 +495,7 @@
@Test
fun progressChangeByUser_insideOfBookendRange_onUpperBookend_doesNotChangeState() = runTest {
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
@@ -514,7 +515,7 @@
@Test
fun touchRelease_onHandleReachedBookend_movesToIdle() = runTest {
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
@@ -531,7 +532,7 @@
@Test
fun onProgressChangeByProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest {
// GIVEN an initialized tracker in the IDLE state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a progress due to an external source that lands at the middle of the slider
val progress = 0.5f
@@ -550,7 +551,7 @@
fun onProgressChangeByProgram_atUpperBookend_onIdle_movesToIdle() = runTest {
// GIVEN an initialized tracker in the IDLE state
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a progress due to an external source that lands at the upper bookend
val progress = config.upperBookendThreshold + 0.01f
@@ -567,7 +568,7 @@
fun onProgressChangeByProgram_atLowerBookend_onIdle_movesToIdle() = runTest {
// GIVEN an initialized tracker in the IDLE state
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// WHEN a progress is recorded due to an external source that lands at the lower bookend
val progress = config.lowerBookendThreshold - 0.01f
@@ -583,7 +584,7 @@
@Test
fun onArrowUp_onArrowMovedOnce_movesToIdle() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
// WHEN the external stimulus is released
@@ -598,7 +599,7 @@
@Test
fun onStartTrackingTouch_onArrowMovedOnce_movesToWait() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
// WHEN the slider starts tracking touch
@@ -615,7 +616,7 @@
@Test
fun onProgressChangeByProgram_onArrowMovedOnce_movesToArrowMovesContinuously() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
// WHEN the slider gets an external progress change
@@ -634,7 +635,7 @@
@Test
fun onArrowUp_onArrowMovesContinuously_movesToIdle() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the external stimulus is released
@@ -649,7 +650,7 @@
@Test
fun onStartTrackingTouch_onArrowMovesContinuously_movesToWait() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider starts tracking touch
@@ -665,7 +666,7 @@
@Test
fun onProgressChangeByProgram_onArrowMovesContinuously_preservesState() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
- initTracker(testScheduler)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider changes progress programmatically at the middle
@@ -684,7 +685,7 @@
fun onProgramProgress_atLowerBookend_onArrowMovesContinuously_movesToIdle() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider reaches the lower bookend programmatically
@@ -702,7 +703,7 @@
fun onProgramProgress_atUpperBookend_onArrowMovesContinuously_movesToIdle() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
val config = SeekableSliderTrackerConfig()
- initTracker(testScheduler, config)
+ initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider reaches the lower bookend programmatically
@@ -718,16 +719,11 @@
@OptIn(ExperimentalCoroutinesApi::class)
private fun initTracker(
- scheduler: TestCoroutineScheduler,
+ scope: CoroutineScope,
config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
) {
mSeekableSliderTracker =
- SeekableSliderTracker(
- sliderStateListener,
- sliderEventProducer,
- UnconfinedTestDispatcher(scheduler),
- config
- )
+ SeekableSliderTracker(sliderStateListener, sliderEventProducer, scope, config)
mSeekableSliderTracker.startTracking()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 14cae0b..24cf164 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -94,7 +94,6 @@
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.Flags;
import com.android.systemui.flags.SystemPropertiesHelper;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
@@ -754,7 +753,7 @@
@Test
public void testUpdateIsKeyguardAfterOccludeAnimationEnds() {
- mViewMediator.mOccludeAnimationController.onLaunchAnimationEnd(
+ mViewMediator.mOccludeAnimationController.onTransitionAnimationEnd(
false /* isExpandingFullyAbove */);
// Since the updateIsKeyguard call is delayed during the animation, ensure it's called once
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
index 2d9d5ed..0e9197e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -181,6 +181,31 @@
}
@Test
+ fun usesOnStepToDoubleValueWithState() =
+ testScope.runTest {
+ val flow =
+ underTest.sharedFlowWithState(
+ duration = 1000.milliseconds,
+ onStep = { it * 2 },
+ )
+ val animationValues by collectLastValue(flow)
+ runCurrent()
+
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.STARTED, 0f))
+ repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 0.6f))
+ repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 1.2f))
+ repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 1.6f))
+ repository.sendTransitionStep(step(1f, TransitionState.RUNNING))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.RUNNING, 2f))
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+ assertThat(animationValues).isEqualTo(StateToValue(TransitionState.FINISHED, null))
+ }
+
+ @Test
fun sameFloatValueWithTheSameTransitionStateDoesNotEmitTwice() =
testScope.runTest {
val flow =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
index 1c9c942..bfa8433 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToAodTransitionViewModelTest.kt
@@ -26,6 +26,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.StateToValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.collect.Range
@@ -57,17 +58,19 @@
// The animation should only start > .4f way through
repository.sendTransitionStep(step(0f, TransitionState.STARTED))
- assertThat(enterFromTopTranslationY).isEqualTo(pixels)
+ assertThat(enterFromTopTranslationY)
+ .isEqualTo(StateToValue(TransitionState.STARTED, pixels))
- repository.sendTransitionStep(step(0.4f))
- assertThat(enterFromTopTranslationY).isEqualTo(pixels)
+ repository.sendTransitionStep(step(.55f))
+ assertThat(enterFromTopTranslationY!!.value ?: -1f).isIn(Range.closed(pixels, 0f))
repository.sendTransitionStep(step(.85f))
- assertThat(enterFromTopTranslationY).isIn(Range.closed(pixels, 0f))
+ assertThat(enterFromTopTranslationY!!.value ?: -1f).isIn(Range.closed(pixels, 0f))
// At the end, the translation should be complete and set to zero
repository.sendTransitionStep(step(1f))
- assertThat(enterFromTopTranslationY).isEqualTo(0f)
+ assertThat(enterFromTopTranslationY)
+ .isEqualTo(StateToValue(TransitionState.RUNNING, 0f))
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
index 301d887..d9453d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
@@ -33,6 +33,7 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
manager = NearbyMediaDevicesManager(commandQueue, logger)
+ manager.start()
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
verify(commandQueue).addCallback(callbackCaptor.capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
index d757d71..ab90b9b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
@@ -24,10 +24,13 @@
import com.android.settingslib.RestrictedLockUtils
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin
+import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.BrightnessMirrorController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -35,6 +38,7 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.isNull
@@ -61,7 +65,7 @@
@Mock
private lateinit var listener: ToggleSlider.Listener
@Mock
- private lateinit var mBrightnessSliderHapticPlugin: BrightnessSliderHapticPlugin
+ private lateinit var vibratorHelper: VibratorHelper
@Captor
private lateinit var seekBarChangeCaptor: ArgumentCaptor<SeekBar.OnSeekBarChangeListener>
@@ -69,6 +73,7 @@
private lateinit var seekBar: SeekBar
private val uiEventLogger = UiEventLoggerFake()
private var mFalsingManager: FalsingManagerFake = FalsingManagerFake()
+ private val systemClock = FakeSystemClock()
private lateinit var mController: BrightnessSliderController
@@ -78,13 +83,14 @@
whenever(mirrorController.toggleSlider).thenReturn(mirror)
whenever(motionEvent.copy()).thenReturn(motionEvent)
+ whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0))
mController =
BrightnessSliderController(
brightnessSliderView,
mFalsingManager,
uiEventLogger,
- mBrightnessSliderHapticPlugin,
+ SeekableSliderHapticPlugin(vibratorHelper, systemClock),
)
mController.init()
mController.setOnChangedListener(listener)
@@ -100,7 +106,6 @@
mController.onViewAttached()
verify(brightnessSliderView).setOnSeekBarChangeListener(notNull())
- verify(mBrightnessSliderHapticPlugin).start()
}
@Test
@@ -110,7 +115,6 @@
verify(brightnessSliderView).setOnSeekBarChangeListener(isNull())
verify(brightnessSliderView).setOnDispatchTouchEventListener(isNull())
- verify(mBrightnessSliderHapticPlugin).stop()
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImplTest.kt
deleted file mode 100644
index 51629b5..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderHapticPluginImplTest.kt
+++ /dev/null
@@ -1,102 +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.settings.brightness
-
-import android.view.VelocityTracker
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.haptics.slider.SeekableSliderEventProducer
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class BrightnessSliderHapticPluginImplTest : SysuiTestCase() {
-
- @Mock private lateinit var vibratorHelper: VibratorHelper
- @Mock private lateinit var velocityTracker: VelocityTracker
- @Mock private lateinit var mainDispatcher: CoroutineDispatcher
-
- private val systemClock = FakeSystemClock()
- private val sliderEventProducer = SeekableSliderEventProducer()
-
- private lateinit var plugin: BrightnessSliderHapticPluginImpl
-
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
- whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0))
- }
-
- @Test
- fun start_beginsTrackingSlider() = runTest {
- createPlugin(UnconfinedTestDispatcher(testScheduler))
- plugin.start()
-
- assertThat(plugin.isTracking).isTrue()
- }
-
- @Test
- fun stop_stopsTrackingSlider() = runTest {
- createPlugin(UnconfinedTestDispatcher(testScheduler))
- // GIVEN that the plugin started the tracking component
- plugin.start()
-
- // WHEN called to stop
- plugin.stop()
-
- // THEN the tracking component stops
- assertThat(plugin.isTracking).isFalse()
- }
-
- @Test
- fun start_afterStop_startsTheTrackingAgain() = runTest {
- createPlugin(UnconfinedTestDispatcher(testScheduler))
- // GIVEN that the plugin started the tracking component
- plugin.start()
-
- // WHEN the plugin is restarted
- plugin.stop()
- plugin.start()
-
- // THEN the tracking begins again
- assertThat(plugin.isTracking).isTrue()
- }
-
- private fun createPlugin(dispatcher: CoroutineDispatcher) {
- plugin =
- BrightnessSliderHapticPluginImpl(
- vibratorHelper,
- systemClock,
- dispatcher,
- velocityTracker,
- sliderEventProducer,
- )
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt
index cd74410..f58ff0a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerTest.kt
@@ -115,7 +115,7 @@
@Test
fun testHunIsRemovedAndCallbackIsInvokedWhenAnimationEnds() {
flagNotificationAsHun()
- controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove = true)
assertFalse(HeadsUpUtil.isClickedHeadsUpNotification(notification))
assertFalse(notification.entry.isExpandAnimationRunning)
@@ -157,7 +157,7 @@
assertNotSame(GROUP_ALERT_SUMMARY, summary.sbn.notification.groupAlertBehavior)
assertNotSame(GROUP_ALERT_SUMMARY, notification.entry.sbn.notification.groupAlertBehavior)
- controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove = true)
verify(headsUpManager)
.removeNotification(summary.key, true /* releaseImmediately */, false /* animate */)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 7589a49..354f3f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -797,6 +797,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_remoteInput() {
ArgumentCaptor<RemoteInputController.Callback> callbackCaptor =
ArgumentCaptor.forClass(RemoteInputController.Callback.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 4afcc8c..04f3216 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -440,6 +440,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_noNotifications() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -451,6 +452,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_remoteInput() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -467,6 +469,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_withoutNotifications() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -482,6 +485,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_oneClearableNotification() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -497,6 +501,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_withoutHistory() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -513,6 +518,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_oneClearableNotification_beforeUserSetup() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(false);
@@ -528,6 +534,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateFooter_oneNonClearableNotification() {
setBarStateForTest(StatusBarState.SHADE);
mStackScroller.setCurrentUserSetup(true);
@@ -544,9 +551,7 @@
}
@Test
- public void testUpdateFooter_atEnd() {
- mStackScroller.setCurrentUserSetup(true);
-
+ public void testFooterPosition_atEnd() {
// add footer
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
@@ -559,8 +564,6 @@
// Expecting the footer to be the last child
int expected = mStackScroller.getChildCount() - 1;
-
- // move footer to end
verify(mStackScroller).changeViewPosition(any(FooterView.class), eq(expected));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 88e4f5a..3a7659d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -27,19 +27,27 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
import com.android.systemui.statusbar.policy.fakeConfigurationController
import com.android.systemui.testKosmos
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.value
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
@@ -58,11 +66,16 @@
fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
}
private val testScope = kosmos.testScope
+
private val activeNotificationListRepository = kosmos.activeNotificationListRepository
- private val fakeKeyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
- private val fakeShadeRepository = kosmos.fakeShadeRepository
- private val zenModeRepository = kosmos.zenModeRepository
private val fakeConfigurationController = kosmos.fakeConfigurationController
+ private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository
+ private val fakeKeyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val fakePowerRepository = kosmos.fakePowerRepository
+ private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
+ private val fakeShadeRepository = kosmos.fakeShadeRepository
+ private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
+ private val zenModeRepository = kosmos.zenModeRepository
val underTest = kosmos.notificationListViewModel
@@ -273,4 +286,193 @@
assertThat(hasFilteredNotifs).isFalse()
}
+
+ @Test
+ fun testShouldShowFooterView_trueWhenShade() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ runCurrent()
+
+ // THEN footer is visible
+ assertThat(shouldShow?.value).isTrue()
+ }
+
+ @Test
+ fun testShouldShowFooterView_trueWhenLockedShade() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open on lockscreen
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ runCurrent()
+
+ // THEN footer is visible
+ assertThat(shouldShow?.value).isTrue()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenKeyguard() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND is on keyguard
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenUserNotSetUp() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ // AND user is not set up
+ fakeUserSetupRepository.setUserSetUp(false)
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenStartingToSleep() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ // AND device is starting to go to sleep
+ fakePowerRepository.updateWakefulness(WakefulnessState.STARTING_TO_SLEEP)
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenQsExpandedDefault() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ // AND quick settings are expanded
+ fakeShadeRepository.setQsExpansion(1f)
+ fakeShadeRepository.legacyQsFullscreen.value = true
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_trueWhenQsExpandedSplitShade() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ // AND quick settings are expanded
+ fakeShadeRepository.setQsExpansion(1f)
+ // AND split shade is enabled
+ overrideResource(R.bool.config_use_split_notification_shade, true)
+ fakeConfigurationController.notifyConfigurationChanged()
+ runCurrent()
+
+ // THEN footer is visible
+ assertThat(shouldShow?.value).isTrue()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenRemoteInputActive() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ // AND remote input is active
+ fakeRemoteInputRepository.isRemoteInputActive.value = true
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_falseWhenShadeIsClosed() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is closed
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(0f)
+ runCurrent()
+
+ // THEN footer is not visible
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ fun testShouldShowFooterView_animatesWhenShade() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND shade is open and fully expanded
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ runCurrent()
+
+ // THEN footer visibility animates
+ assertThat(shouldShow?.isAnimating).isTrue()
+ }
+
+ @Test
+ fun testShouldShowFooterView_notAnimatingOnKeyguard() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND we are on the keyguard
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+ fakeShadeRepository.setLegacyShadeExpansion(1f)
+ runCurrent()
+
+ // THEN footer visibility does not animate
+ assertThat(shouldShow?.isAnimating).isFalse()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index b6a033a..1b43851 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -46,6 +46,7 @@
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.process.processWrapper
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -1147,6 +1148,7 @@
uiEventLogger = uiEventLogger,
featureFlags = kosmos.fakeFeatureFlagsClassic,
userRestrictionChecker = mock(),
+ processWrapper = kosmos.processWrapper,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index 21d4549..661837b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -34,6 +34,7 @@
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.process.ProcessWrapperFake
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -264,6 +265,7 @@
guestUserInteractor = guestUserInteractor,
uiEventLogger = uiEventLogger,
userRestrictionChecker = mock(),
+ processWrapper = ProcessWrapperFake()
)
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index d0804be..5661e20 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -34,6 +34,7 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.process.ProcessWrapperFake
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -176,6 +177,7 @@
guestUserInteractor = guestUserInteractor,
uiEventLogger = uiEventLogger,
userRestrictionChecker = mock(),
+ processWrapper = ProcessWrapperFake()
),
guestUserInteractor = guestUserInteractor,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 7a8dce8..8a33778 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -64,7 +64,6 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.testing.UiEventLoggerFake;
-import com.android.keyguard.TestScopeProvider;
import com.android.systemui.Prefs;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.AnimatorTestRule;
@@ -102,8 +101,6 @@
import java.util.Arrays;
import java.util.function.Predicate;
-import kotlinx.coroutines.Dispatchers;
-
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -208,8 +205,6 @@
mDumpManager,
mLazySecureSettings,
mVibratorHelper,
- Dispatchers.getUnconfined(),
- TestScopeProvider.getTestScope(),
new FakeSystemClock());
mDialog.init(0, null);
State state = createShellState();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
index f723a9e5..5b84a41 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
@@ -36,7 +36,7 @@
object : AnimationFeatureFlags {
override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim
},
- launchAnimator = fakeLaunchAnimator(),
+ transitionAnimator = fakeTransitionAnimator(),
isForTesting = true,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeLaunchAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
similarity index 79%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeLaunchAnimator.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
index 0983041..bc7ec3f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeLaunchAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
@@ -16,19 +16,19 @@
import com.android.app.animation.Interpolators
-/** A [LaunchAnimator] to be used in tests. */
-fun fakeLaunchAnimator(): LaunchAnimator {
- return LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
+/** A [TransitionAnimator] to be used in tests. */
+fun fakeTransitionAnimator(): TransitionAnimator {
+ return TransitionAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
}
/**
- * A [LaunchAnimator.Timings] to be used in tests.
+ * A [TransitionAnimator.Timings] to be used in tests.
*
* Note that all timings except the total duration are non-zero to avoid divide-by-zero exceptions
* when computing the progress of a sub-animation (the contents fade in/out).
*/
private val TEST_TIMINGS =
- LaunchAnimator.Timings(
+ TransitionAnimator.Timings(
totalDuration = 0L,
contentBeforeFadeOutDelay = 1L,
contentBeforeFadeOutDuration = 1L,
@@ -36,9 +36,9 @@
contentAfterFadeInDuration = 1L
)
-/** A [LaunchAnimator.Interpolators] to be used in tests. */
+/** A [TransitionAnimator.Interpolators] to be used in tests. */
private val TEST_INTERPOLATORS =
- LaunchAnimator.Interpolators(
+ TransitionAnimator.Interpolators(
positionInterpolator = Interpolators.STANDARD,
positionXInterpolator = Interpolators.STANDARD,
contentBeforeFadeOutInterpolator = Interpolators.STANDARD,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt
index 21cff0d..3b3e23e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FaceWakeUpTriggersConfigKosmos.kt
@@ -18,5 +18,7 @@
import com.android.systemui.kosmos.Kosmos
+var Kosmos.fakeFaceWakeUpTriggersConfig by Kosmos.Fixture { FakeFaceWakeUpTriggersConfig() }
+
var Kosmos.faceWakeUpTriggersConfig: FaceWakeUpTriggersConfig by
- Kosmos.Fixture { FakeFaceWakeUpTriggersConfig() }
+ Kosmos.Fixture { fakeFaceWakeUpTriggersConfig }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
index 5575b05..a8fc27a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt
@@ -27,7 +27,6 @@
import com.android.systemui.deviceentry.data.repository.faceWakeUpTriggersConfig
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -51,7 +50,7 @@
keyguardTransitionInteractor = keyguardTransitionInteractor,
faceAuthenticationLogger = faceAuthLogger,
keyguardUpdateMonitor = keyguardUpdateMonitor,
- deviceEntryFingerprintAuthRepository = deviceEntryFingerprintAuthRepository,
+ deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
userRepository = userRepository,
facePropertyRepository = facePropertyRepository,
faceWakeUpTriggersConfig = faceWakeUpTriggersConfig,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
index a8f45b0..6f168d4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
@@ -33,6 +33,7 @@
keyguardInteractor = keyguardInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
goneToAodTransitionViewModel = goneToAodTransitionViewModel,
+ aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
keyguardClockViewModel = keyguardClockViewModel,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt
index 9841778..dee3644 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt
@@ -16,11 +16,17 @@
package com.android.systemui.process
+import android.os.UserHandle
+
class ProcessWrapperFake : ProcessWrapper() {
var systemUser: Boolean = false
+ var userHandle: UserHandle = UserHandle.getUserHandleForUid(0)
+
override fun isSystemUser(): Boolean {
return systemUser
}
+
+ override fun myUserHandle() = userHandle
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
index e67df9d..8e430db 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
@@ -26,6 +26,8 @@
class FakeQSSceneAdapter(
private val inflateDelegate: suspend (Context) -> View,
+ override val qqsHeight: Int = 0,
+ override val qsHeight: Int = 0,
) : QSSceneAdapter {
private val _customizing = MutableStateFlow(false)
override val isCustomizing: StateFlow<Boolean> = _customizing.asStateFlow()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index 998e579..37b2b76 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -16,10 +16,14 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.shade.domain.interactor.shadeAnimationInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel
@@ -30,14 +34,18 @@
val Kosmos.notificationListViewModel by Fixture {
NotificationListViewModel(
- shelf = notificationShelfViewModel,
- hideListViewModel = hideListViewModel,
- footer = Optional.of(footerViewModel),
- logger = Optional.of(notificationListLoggerViewModel),
- activeNotificationsInteractor = activeNotificationsInteractor,
- keyguardTransitionInteractor = keyguardTransitionInteractor,
- seenNotificationsInteractor = seenNotificationsInteractor,
- shadeInteractor = shadeInteractor,
- zenModeInteractor = zenModeInteractor,
+ notificationShelfViewModel,
+ hideListViewModel,
+ Optional.of(footerViewModel),
+ Optional.of(notificationListLoggerViewModel),
+ activeNotificationsInteractor,
+ keyguardInteractor,
+ keyguardTransitionInteractor,
+ powerInteractor,
+ remoteInputInteractor,
+ seenNotificationsInteractor,
+ shadeInteractor,
+ userSetupInteractor,
+ zenModeInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
index 4e2dc7a..1504df4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
@@ -28,6 +28,7 @@
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.plugins.activityStarter
+import com.android.systemui.process.processWrapper
import com.android.systemui.telephony.domain.interactor.telephonyInteractor
import com.android.systemui.user.data.repository.userRepository
import com.android.systemui.utils.userRestrictionChecker
@@ -53,5 +54,6 @@
guestUserInteractor = guestUserInteractor,
uiEventLogger = uiEventLogger,
userRestrictionChecker = userRestrictionChecker,
+ processWrapper = processWrapper,
)
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
index 78f07e4..a11cf8c 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
@@ -818,7 +818,7 @@
}
// Don't need to add the embedded hierarchy windows into the accessibility windows list.
- if (mHostEmbeddedMap.size() > 0 && isEmbeddedHierarchyWindowsLocked(windowId)) {
+ if (isEmbeddedHierarchyWindowsLocked(windowId)) {
return null;
}
final AccessibilityWindowInfo reportedWindow = AccessibilityWindowInfo.obtain();
@@ -866,21 +866,6 @@
return reportedWindow;
}
- private boolean isEmbeddedHierarchyWindowsLocked(int windowId) {
- final IBinder leashToken = mWindowIdMap.get(windowId);
- if (leashToken == null) {
- return false;
- }
-
- for (int i = 0; i < mHostEmbeddedMap.size(); i++) {
- if (mHostEmbeddedMap.keyAt(i).equals(leashToken)) {
- return true;
- }
- }
-
- return false;
- }
-
private int getTypeForWindowManagerWindowType(int windowType) {
switch (windowType) {
case WindowManager.LayoutParams.TYPE_APPLICATION:
@@ -1490,7 +1475,7 @@
* @return The windowId of the parent window, or self if no parent exists
*/
public int resolveParentWindowIdLocked(int windowId) {
- final IBinder token = getTokenLocked(windowId);
+ final IBinder token = getLeashTokenLocked(windowId);
if (token == null) {
return windowId;
}
@@ -2095,7 +2080,7 @@
* @param windowId The windowID.
* @return The token, or {@code NULL} if this windowID doesn't exist
*/
- IBinder getTokenLocked(int windowId) {
+ IBinder getLeashTokenLocked(int windowId) {
return mWindowIdMap.get(windowId);
}
@@ -2124,6 +2109,23 @@
}
/**
+ * Checks if the window is embedded into another window so that the window should be excluded
+ * from the exposed accessibility windows, and the node tree should be embedded in the host.
+ */
+ boolean isEmbeddedHierarchyWindowsLocked(int windowId) {
+ if (mHostEmbeddedMap.size() == 0) {
+ return false;
+ }
+
+ final IBinder leashToken = getLeashTokenLocked(windowId);
+ if (leashToken == null) {
+ return false;
+ }
+
+ return mHostEmbeddedMap.containsKey(leashToken);
+ }
+
+ /**
* Checks if the window belongs to a proxy display and if so clears the focused window id.
* @param focusClearedWindowId the cleared window id.
* @return true if an observer is proxy-ed and has cleared its focused window id.
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 83d9cdb..2ff23ee 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -16,6 +16,8 @@
package com.android.server.autofill;
+import static android.credentials.Constants.FAILURE_CREDMAN_SELECTOR;
+import static android.credentials.Constants.SUCCESS_CREDMAN_SELECTOR;
import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES;
import static android.service.autofill.AutofillService.EXTRA_FILL_RESPONSE;
import static android.service.autofill.AutofillService.WEBVIEW_REQUESTED_CREDENTIAL_KEY;
@@ -113,6 +115,8 @@
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.ServiceInfo;
+import android.credentials.GetCredentialException;
+import android.credentials.GetCredentialResponse;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -123,10 +127,12 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
+import android.os.Parcel;
import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteCallback;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.os.SystemClock;
import android.service.assist.classification.FieldClassificationRequest;
import android.service.assist.classification.FieldClassificationResponse;
@@ -153,6 +159,7 @@
import android.service.autofill.SaveRequest;
import android.service.autofill.UserData;
import android.service.autofill.ValueFinder;
+import android.service.credentials.CredentialProviderService;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -2507,7 +2514,7 @@
+ id + " destroyed");
return;
}
- fillInIntent = createAuthFillInIntentLocked(requestId, extras, /* authExtras= */ null);
+ fillInIntent = createAuthFillInIntentLocked(requestId, extras);
if (fillInIntent == null) {
forceRemoveFromServiceLocked();
return;
@@ -2808,6 +2815,7 @@
mSessionFlags.mExpiredResponse = false;
final Parcelable result = data.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT);
+
final Bundle newClientState = data.getBundle(AutofillManager.EXTRA_CLIENT_STATE);
if (sDebug) {
Slog.d(TAG, "setAuthenticationResultLocked(): result=" + result
@@ -2818,6 +2826,12 @@
mPresentationStatsEventLogger.maybeSetAuthenticationResult(
AUTHENTICATION_RESULT_SUCCESS);
replaceResponseLocked(authenticatedResponse, (FillResponse) result, newClientState);
+ } else if (result instanceof GetCredentialResponse) {
+ Slog.d(TAG, "Received GetCredentialResponse from authentication flow");
+ Dataset dataset = getDatasetFromCredentialResponse((GetCredentialResponse) result);
+ if (dataset != null) {
+ autoFill(requestId, datasetIdx, dataset, false, UI_TYPE_UNKNOWN);
+ }
} else if (result instanceof Dataset) {
if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) {
logAuthenticationStatusLocked(requestId,
@@ -2854,6 +2868,17 @@
}
}
+ private Dataset getDatasetFromCredentialResponse(GetCredentialResponse result) {
+ if (result == null) {
+ return null;
+ }
+ Bundle bundle = result.getCredential().getData();
+ if (bundle == null) {
+ return null;
+ }
+ return bundle.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT, Dataset.class);
+ }
+
Dataset getEffectiveDatasetForAuthentication(Dataset authenticatedDataset) {
FillResponse response = new FillResponse.Builder().addDataset(authenticatedDataset).build();
response = getEffectiveFillResponse(response);
@@ -4690,6 +4715,11 @@
}
+ if (isCredmanIntegrationActive(response)) {
+ Slog.d(TAG, "Attempting to add Credential Manager callback to pinned entries");
+ addCredentialManagerCallback(response);
+ }
+
if (response.supportsInlineSuggestions()) {
synchronized (mLock) {
if (requestShowInlineSuggestionsLocked(response, filterText)) {
@@ -4749,6 +4779,11 @@
}
}
+ private boolean isCredmanIntegrationActive(FillResponse response) {
+ return Flags.autofillCredmanIntegration()
+ && (response.getFlags() & FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE) != 0;
+ }
+
@GuardedBy("mLock")
private void updateFillDialogTriggerIdsLocked() {
final FillResponse response = getLastResponseLocked(null);
@@ -4964,6 +4999,69 @@
return mInlineSessionController.setInlineFillUiLocked(inlineFillUi);
}
+ private void addCredentialManagerCallback(FillResponse response) {
+ if (response.getDatasets() == null) {
+ return;
+ }
+ for (Dataset dataset: response.getDatasets()) {
+ if (isPinnedDataset(dataset)) {
+ Slog.d(TAG, "Adding Credential Manager callback to a pinned entry");
+ addCredentialManagerCallbackForDataset(dataset, response.getRequestId());
+ }
+ }
+ }
+
+ private void addCredentialManagerCallbackForDataset(Dataset dataset, int requestId) {
+ final ResultReceiver resultReceiver = new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == SUCCESS_CREDMAN_SELECTOR) {
+ Slog.d(TAG, "onReceiveResult from Credential Manager bottom sheet");
+ GetCredentialResponse getCredentialResponse =
+ resultData.getParcelable(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
+ GetCredentialResponse.class);
+ Dataset datasetFromCredential = getDatasetFromCredentialResponse(
+ getCredentialResponse);
+ if (datasetFromCredential != null) {
+ autoFill(requestId, /*datasetIndex=*/-1,
+ datasetFromCredential, false,
+ UI_TYPE_CREDMAN_BOTTOM_SHEET);
+ }
+ } else if (resultCode == FAILURE_CREDMAN_SELECTOR) {
+ GetCredentialException exception = resultData.getParcelable(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_EXCEPTION,
+ GetCredentialException.class);
+ Slog.d(TAG, "Credman bottom sheet from pinned "
+ + "entry failed with: + " + exception.getType() + " , "
+ + exception.getMessage());
+ } else {
+ Slog.d(TAG, "Unknown resultCode from credential "
+ + "manager bottom sheet: " + resultCode);
+ }
+ }
+ };
+ ResultReceiver ipcFriendlyResultReceiver =
+ toIpcFriendlyResultReceiver(resultReceiver);
+
+ Intent metadataIntent = dataset.getCredentialFillInIntent();
+ metadataIntent.putExtra(
+ android.credentials.selection.Constants.EXTRA_FINAL_RESPONSE_RECEIVER,
+ ipcFriendlyResultReceiver);
+ dataset.setCredentialFillInIntent(metadataIntent);
+ }
+
+ private ResultReceiver toIpcFriendlyResultReceiver(ResultReceiver resultReceiver) {
+ final Parcel parcel = Parcel.obtain();
+ resultReceiver.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ return ipcFriendly;
+ }
+
boolean isDestroyed() {
synchronized (mLock) {
return mDestroyed;
@@ -5669,8 +5767,14 @@
// does not matter the value of isPrimary because null response won't be overridden.
setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH,
/* clearResponse= */ false, /* isPrimary= */ true);
- final Intent fillInIntent = createAuthFillInIntentLocked(requestId, mClientState,
- dataset.getAuthenticationExtras());
+ final Intent fillInIntent;
+ if (dataset.getCredentialFillInIntent() != null && Flags.autofillCredmanIntegration()) {
+ Slog.d(TAG, "Setting credential fill intent");
+ fillInIntent = dataset.getCredentialFillInIntent();
+ } else {
+ fillInIntent = createAuthFillInIntentLocked(requestId, mClientState);
+ }
+
if (fillInIntent == null) {
forceRemoveFromServiceLocked();
return;
@@ -5686,8 +5790,7 @@
// TODO: this should never be null, but we got at least one occurrence, probably due to a race.
@GuardedBy("mLock")
@Nullable
- private Intent createAuthFillInIntentLocked(int requestId, Bundle extras,
- @Nullable Bundle authExtras) {
+ private Intent createAuthFillInIntentLocked(int requestId, Bundle extras) {
final Intent fillInIntent = new Intent();
final FillContext context = getFillContextByRequestIdLocked(requestId);
@@ -5704,9 +5807,6 @@
}
fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, context.getStructure());
fillInIntent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, extras);
- if (authExtras != null) {
- fillInIntent.putExtra(AutofillManager.EXTRA_AUTH_STATE, authExtras);
- }
return fillInIntent;
}
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 8962bf0..1b49f18e 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -692,32 +692,37 @@
private int mInputDeviceId = IInputConstants.INVALID_INPUT_DEVICE_ID;
- WaitForDevice(String deviceName, int vendorId, int productId) {
+ WaitForDevice(String deviceName, int vendorId, int productId, int associatedDisplayId) {
mListener = new InputManager.InputDeviceListener() {
@Override
public void onInputDeviceAdded(int deviceId) {
- final InputDevice device = InputManagerGlobal.getInstance().getInputDevice(
- deviceId);
- Objects.requireNonNull(device, "Newly added input device was null.");
- if (!device.getName().equals(deviceName)) {
- return;
- }
- final InputDeviceIdentifier id = device.getIdentifier();
- if (id.getVendorId() != vendorId || id.getProductId() != productId) {
- return;
- }
- mInputDeviceId = deviceId;
- mDeviceAddedLatch.countDown();
+ onInputDeviceChanged(deviceId);
}
@Override
public void onInputDeviceRemoved(int deviceId) {
-
}
@Override
public void onInputDeviceChanged(int deviceId) {
+ if (isMatchingDevice(deviceId)) {
+ mInputDeviceId = deviceId;
+ mDeviceAddedLatch.countDown();
+ }
+ }
+ private boolean isMatchingDevice(int deviceId) {
+ final InputDevice device = InputManagerGlobal.getInstance().getInputDevice(
+ deviceId);
+ Objects.requireNonNull(device, "Newly added input device was null.");
+ if (!device.getName().equals(deviceName)) {
+ return false;
+ }
+ final InputDeviceIdentifier id = device.getIdentifier();
+ if (id.getVendorId() != vendorId || id.getProductId() != productId) {
+ return false;
+ }
+ return device.getAssociatedDisplayId() == associatedDisplayId;
}
};
InputManagerGlobal.getInstance().registerInputDeviceListener(mListener, mHandler);
@@ -799,7 +804,7 @@
final int inputDeviceId;
setUniqueIdAssociation(displayId, phys);
- try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId)) {
+ try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId, displayId)) {
ptr = deviceOpener.get();
// See INVALID_PTR in libs/input/VirtualInputDevice.cpp.
if (ptr == 0) {
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 9d9e7c9..7979936 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -16,8 +16,8 @@
package com.android.server;
-import static android.os.Flags.stateOfHealthPublic;
import static android.os.Flags.batteryServiceSupportCurrentAdbCommand;
+import static android.os.Flags.stateOfHealthPublic;
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import static com.android.server.health.Utils.copyV1Battery;
@@ -81,6 +81,7 @@
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.NoSuchElementException;
+import java.util.concurrent.CopyOnWriteArraySet;
/**
* <p>BatteryService monitors the charging status, and charge level of the device
@@ -157,6 +158,12 @@
private int mLastChargeCounter;
private int mLastBatteryCycleCount;
private int mLastCharingState;
+ /**
+ * The last seen charging policy. This requires the
+ * {@link android.Manifest.permission#BATTERY_STATS} permission and should therefore not be
+ * included in the ACTION_BATTERY_CHANGED intent extras.
+ */
+ private int mLastChargingPolicy;
private int mSequence = 1;
@@ -197,6 +204,9 @@
private ArrayDeque<Bundle> mBatteryLevelsEventQueue;
private long mLastBatteryLevelChangedSentMs;
+ private final CopyOnWriteArraySet<BatteryManagerInternal.ChargingPolicyChangeListener>
+ mChargingPolicyChangeListeners = new CopyOnWriteArraySet<>();
+
private Bundle mBatteryChangedOptions = BroadcastOptions.makeBasic()
.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
.setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
@@ -527,6 +537,11 @@
shutdownIfNoPowerLocked();
shutdownIfOverTempLocked();
+ if (force || mHealthInfo.chargingPolicy != mLastChargingPolicy) {
+ mLastChargingPolicy = mHealthInfo.chargingPolicy;
+ mHandler.post(this::notifyChargingPolicyChanged);
+ }
+
if (force
|| (mHealthInfo.batteryStatus != mLastBatteryStatus
|| mHealthInfo.batteryHealth != mLastBatteryHealth
@@ -827,6 +842,17 @@
mLastBatteryLevelChangedSentMs = SystemClock.elapsedRealtime();
}
+ private void notifyChargingPolicyChanged() {
+ final int newPolicy;
+ synchronized (mLock) {
+ newPolicy = mLastChargingPolicy;
+ }
+ for (BatteryManagerInternal.ChargingPolicyChangeListener listener
+ : mChargingPolicyChangeListeners) {
+ listener.onChargingPolicyChanged(newPolicy);
+ }
+ }
+
// TODO: Current code doesn't work since "--unplugged" flag in BSS was purposefully removed.
private void logBatteryStatsLocked() {
IBinder batteryInfoService = ServiceManager.getService(BatteryStats.SERVICE_NAME);
@@ -1220,6 +1246,8 @@
pw.println(" voltage: " + mHealthInfo.batteryVoltageMillivolts);
pw.println(" temperature: " + mHealthInfo.batteryTemperatureTenthsCelsius);
pw.println(" technology: " + mHealthInfo.batteryTechnology);
+ pw.println(" Charging state: " + mHealthInfo.chargingState);
+ pw.println(" Charging policy: " + mHealthInfo.chargingPolicy);
} else {
Shell shell = new Shell();
shell.exec(mBinderService, null, fd, null, args, null, new ResultReceiver(null));
@@ -1452,6 +1480,19 @@
}
@Override
+ public void registerChargingPolicyChangeListener(
+ BatteryManagerInternal.ChargingPolicyChangeListener listener) {
+ mChargingPolicyChangeListeners.add(listener);
+ }
+
+ @Override
+ public int getChargingPolicy() {
+ synchronized (mLock) {
+ return mLastChargingPolicy;
+ }
+ }
+
+ @Override
public int getInvalidCharger() {
synchronized (mLock) {
return mInvalidCharger;
diff --git a/services/core/java/com/android/server/DockObserver.java b/services/core/java/com/android/server/DockObserver.java
index 9554e63..fb527c1 100644
--- a/services/core/java/com/android/server/DockObserver.java
+++ b/services/core/java/com/android/server/DockObserver.java
@@ -20,8 +20,9 @@
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
-import android.media.AudioAttributes;
+import android.media.AudioManager;
import android.media.Ringtone;
+import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
@@ -305,16 +306,11 @@
if (soundPath != null) {
final Uri soundUri = Uri.parse("file://" + soundPath);
if (soundUri != null) {
- AudioAttributes audioAttributes = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build();
- final Ringtone sfx = new Ringtone.Builder(getContext(),
- Ringtone.MEDIA_SOUND, audioAttributes)
- .setUri(soundUri)
- .setPreferBuiltinDevice()
- .build();
+ final Ringtone sfx = RingtoneManager.getRingtone(
+ getContext(), soundUri);
if (sfx != null) {
+ sfx.setStreamType(AudioManager.STREAM_SYSTEM);
+ sfx.preferBuiltinDevice(true);
sfx.play();
}
}
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index adc0255..cd45b03 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -581,7 +581,8 @@
if (DEBUG_FOREGROUND_SERVICE) {
Slog.i(TAG, " Stopping fg for service " + r);
}
- setServiceForegroundInnerLocked(r, 0, null, 0, 0);
+ setServiceForegroundInnerLocked(r, 0, null, 0, 0,
+ 0);
}
}
@@ -989,7 +990,7 @@
if (fgRequired) {
logFgsBackgroundStart(r);
- if (!r.isFgsAllowedStart() && isBgFgsRestrictionEnabled(r)) {
+ if (!r.isFgsAllowedStart() && isBgFgsRestrictionEnabled(r, callingUid)) {
String msg = "startForegroundService() not allowed due to "
+ "mAllowStartForeground false: service "
+ r.shortInstanceName;
@@ -1787,11 +1788,13 @@
public void setServiceForegroundLocked(ComponentName className, IBinder token,
int id, Notification notification, int flags, int foregroundServiceType) {
final int userId = UserHandle.getCallingUserId();
+ final int callingUid = mAm.mInjector.getCallingUid();
final long origId = mAm.mInjector.clearCallingIdentity();
try {
ServiceRecord r = findServiceLocked(className, token, userId);
if (r != null) {
- setServiceForegroundInnerLocked(r, id, notification, flags, foregroundServiceType);
+ setServiceForegroundInnerLocked(r, id, notification, flags, foregroundServiceType,
+ callingUid);
}
} finally {
mAm.mInjector.restoreCallingIdentity(origId);
@@ -2106,7 +2109,8 @@
*/
@GuardedBy("mAm")
private void setServiceForegroundInnerLocked(final ServiceRecord r, int id,
- Notification notification, int flags, int foregroundServiceType) {
+ Notification notification, int flags, int foregroundServiceType,
+ int callingUidIfStart) {
if (id != 0) {
if (notification == null) {
throw new IllegalArgumentException("null notification");
@@ -2234,7 +2238,8 @@
}
// Whether FGS-BG-start restriction is enabled for this service.
- final boolean isBgFgsRestrictionEnabledForService = isBgFgsRestrictionEnabled(r);
+ final boolean isBgFgsRestrictionEnabledForService = isBgFgsRestrictionEnabled(r,
+ callingUidIfStart);
// Whether to extend the SHORT_SERVICE time out.
boolean extendShortServiceTimeout = false;
@@ -8486,14 +8491,43 @@
NOTE_FOREGROUND_SERVICE_BG_LAUNCH, n.build(), UserHandle.ALL);
}
- private boolean isBgFgsRestrictionEnabled(ServiceRecord r) {
- return mAm.mConstants.mFlagFgsStartRestrictionEnabled
- // Checking service's targetSdkVersion.
- && CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, r.appInfo.uid)
- && (!mAm.mConstants.mFgsStartRestrictionCheckCallerTargetSdk
- // Checking callingUid's targetSdkVersion.
- || CompatChanges.isChangeEnabled(
- FGS_BG_START_RESTRICTION_CHANGE_ID, r.mRecentCallingUid));
+ private boolean isBgFgsRestrictionEnabled(ServiceRecord r, int actualCallingUid) {
+ // mFlagFgsStartRestrictionEnabled controls whether to enable the BG FGS restrictions:
+ // - If true (default), BG-FGS restrictions are enabled if the service targets >= S.
+ // - If false, BG-FGS restrictions are disabled for all apps.
+ if (!mAm.mConstants.mFlagFgsStartRestrictionEnabled) {
+ return false;
+ }
+
+ // If the service target below S, then don't enable the restrictions.
+ if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, r.appInfo.uid)) {
+ return false;
+ }
+
+ // mFgsStartRestrictionCheckCallerTargetSdk controls whether we take the caller's target
+ // SDK level into account or not:
+ // - If true (default), BG-FGS restrictions only happens if the caller _also_ targets >= S.
+ // - If false, BG-FGS restrictions do _not_ use the caller SDK levels.
+ if (!mAm.mConstants.mFgsStartRestrictionCheckCallerTargetSdk) {
+ return true; // In this case, we only check the service's target SDK level.
+ }
+ final int callingUid;
+ if (Flags.newFgsRestrictionLogic()) {
+ // We always consider SYSTEM_UID to target S+, so just enable the restrictions.
+ if (actualCallingUid == Process.SYSTEM_UID) {
+ return true;
+ }
+ callingUid = actualCallingUid;
+ } else {
+ // Legacy logic used mRecentCallingUid.
+ callingUid = r.mRecentCallingUid;
+ }
+ if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, callingUid)) {
+ return false; // If the caller targets < S, then we still disable the restrictions.
+ }
+
+ // Both the service and the caller target S+, so enable the check.
+ return true;
}
private void logFgsBackgroundStart(ServiceRecord r) {
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index 458fd82..05e681e 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -1020,6 +1020,10 @@
}
}
+ private boolean isAutomotive() {
+ return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ }
+
private Set<Integer> getEnabledUserHandles(int currentUserHandle) {
int[] userProfiles = mUserManager.getEnabledProfileIds(currentUserHandle);
Set<Integer> handles = new ArraySet<>(userProfiles.length);
@@ -1030,8 +1034,8 @@
if (Flags.cameraHsumPermission()) {
// If the device is running in headless system user mode then allow
- // User 0 to access camera.
- if (UserManager.isHeadlessSystemUserMode()) {
+ // User 0 to access camera only for automotive form factor.
+ if (UserManager.isHeadlessSystemUserMode() && isAutomotive()) {
handles.add(UserHandle.USER_SYSTEM);
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 91706cf..7dbe880 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -727,7 +727,7 @@
private static final int MY_UID = Process.myUid();
private static final int MY_PID = Process.myPid();
- private static final IBinder ALLOWLIST_TOKEN = new Binder();
+ static final IBinder ALLOWLIST_TOKEN = new Binder();
protected RankingHandler mRankingHandler;
private long mLastOverRateLogTime;
private float mMaxPackageEnqueueRate = DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE;
@@ -4759,7 +4759,7 @@
// Remove background token before returning notification to untrusted app, this
// ensures the app isn't able to perform background operations that are
// associated with notification interactions.
- notification.clearAllowlistToken();
+ notification.overrideAllowlistToken(null);
return new StatusBarNotification(
sbn.getPackageName(),
sbn.getOpPkg(),
@@ -7623,6 +7623,8 @@
}
}
+ notification.overrideAllowlistToken(ALLOWLIST_TOKEN);
+
// Remote views? Are they too big?
checkRemoteViews(pkg, tag, id, notification);
}
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java
new file mode 100644
index 0000000..1553618
--- /dev/null
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java
@@ -0,0 +1,97 @@
+/*
+ * 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.server.pm;
+
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+
+import android.annotation.NonNull;
+import android.app.BackgroundInstallControlManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IRemoteCallback;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ServiceThread;
+
+public class BackgroundInstallControlCallbackHelper {
+
+ @VisibleForTesting static final String FLAGGED_PACKAGE_NAME_KEY = "packageName";
+ @VisibleForTesting static final String FLAGGED_USER_ID_KEY = "userId";
+ private static final String TAG = "BackgroundInstallControlCallbackHelper";
+
+ private final Handler mHandler;
+
+ BackgroundInstallControlCallbackHelper() {
+ HandlerThread backgroundThread =
+ new ServiceThread(
+ "BackgroundInstallControlCallbackHelperBg",
+ THREAD_PRIORITY_BACKGROUND,
+ true);
+ backgroundThread.start();
+ mHandler = new Handler(backgroundThread.getLooper());
+ }
+
+ @NonNull @VisibleForTesting
+ final RemoteCallbackList<IRemoteCallback> mCallbacks = new RemoteCallbackList<>();
+
+ /** Registers callback that gets invoked upon detection of an MBA
+ *
+ * NOTE: The callback is user context agnostic and currently broadcasts to all users of other
+ * users app installs. This is fine because the API is for SystemServer use only.
+ */
+ public void registerBackgroundInstallCallback(IRemoteCallback callback) {
+ synchronized (mCallbacks) {
+ mCallbacks.register(callback, null);
+ }
+ }
+
+ /** Unregisters callback */
+ public void unregisterBackgroundInstallCallback(IRemoteCallback callback) {
+ synchronized (mCallbacks) {
+ mCallbacks.unregister(callback);
+ }
+ }
+
+ /**
+ * Invokes all registered callbacks Callbacks are processed through user provided-threads and
+ * parameters are passed in via {@link BackgroundInstallControlManager} InstallEvent
+ */
+ public void notifyAllCallbacks(int userId, String packageName) {
+ Bundle extras = new Bundle();
+ extras.putCharSequence(FLAGGED_PACKAGE_NAME_KEY, packageName);
+ extras.putInt(FLAGGED_USER_ID_KEY, userId);
+ synchronized (mCallbacks) {
+ mHandler.post(
+ () ->
+ mCallbacks.broadcast(
+ callback -> {
+ try {
+ callback.sendResult(extras);
+ } catch (RemoteException e) {
+ Slog.e(
+ TAG,
+ "error detected: " + e.getLocalizedMessage(),
+ e);
+ }
+ }));
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index 200b17b..3468081 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -36,6 +36,7 @@
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
+import android.os.IRemoteCallback;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
@@ -93,6 +94,8 @@
private final File mDiskFile;
private final Context mContext;
+ private final BackgroundInstallControlCallbackHelper mCallbackHelper;
+
private SparseSetArray<String> mBackgroundInstalledPackages = null;
// User ID -> package name -> set of foreground time frame
@@ -112,6 +115,7 @@
mHandler = new EventHandler(injector.getLooper(), this);
mDiskFile = injector.getDiskFile();
mContext = injector.getContext();
+ mCallbackHelper = injector.getBackgroundInstallControlCallbackHelper();
UsageStatsManagerInternal usageStatsManagerInternal =
injector.getUsageStatsManagerInternal();
usageStatsManagerInternal.registerListener(
@@ -150,6 +154,16 @@
}
}
+ @Override
+ public void registerBackgroundInstallCallback(IRemoteCallback callback) {
+ mService.mCallbackHelper.registerBackgroundInstallCallback(callback);
+ }
+
+ @Override
+ public void unregisterBackgroundInstallCallback(IRemoteCallback callback) {
+ mService.mCallbackHelper.unregisterBackgroundInstallCallback(callback);
+ }
+
}
@RequiresPermission(GET_BACKGROUND_INSTALLED_PACKAGES)
@@ -274,6 +288,7 @@
initBackgroundInstalledPackages();
mBackgroundInstalledPackages.add(userId, packageName);
+ mCallbackHelper.notifyAllCallbacks(userId, packageName);
writeBackgroundInstalledPackagesToDisk();
}
@@ -568,6 +583,8 @@
File getDiskFile();
+ BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper();
+
}
private static final class InjectorImpl implements Injector {
@@ -617,5 +634,10 @@
File file = new File(dir, DISK_FILE_NAME);
return file;
}
+
+ @Override
+ public BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper() {
+ return new BackgroundInstallControlCallbackHelper();
+ }
}
}
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 33f481c..db5acc2 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -592,9 +592,11 @@
mPm.addAllPackageProperties(pkg);
if (oldPkgSetting == null || oldPkgSetting.getPkg() == null) {
- mPm.mDomainVerificationManager.addPackage(pkgSetting);
+ mPm.mDomainVerificationManager.addPackage(pkgSetting,
+ request.getPreVerifiedDomains());
} else {
- mPm.mDomainVerificationManager.migrateState(oldPkgSetting, pkgSetting);
+ mPm.mDomainVerificationManager.migrateState(oldPkgSetting, pkgSetting,
+ request.getPreVerifiedDomains());
}
int collectionSize = ArrayUtils.size(pkg.getInstrumentations());
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index b8960da..afd4fb1 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -569,7 +569,8 @@
* target sdk apps as malware can target older sdk versions to avoid
* the enforcement of new API behavior.
*/
- public static final int MIN_INSTALLABLE_TARGET_SDK = Build.VERSION_CODES.M;
+ public static final int MIN_INSTALLABLE_TARGET_SDK =
+ Flags.minTargetSdk24() ? Build.VERSION_CODES.N : Build.VERSION_CODES.M;
// Compilation reasons.
// TODO(b/260124949): Clean this up with the legacy dexopt code.
@@ -6483,6 +6484,17 @@
}
@Override
+ @Nullable
+ public ComponentName getDomainVerificationAgent() {
+ final int callerUid = Binder.getCallingUid();
+ if (!PackageManagerServiceUtils.isRootOrShell(callerUid)) {
+ throw new SecurityException("Not allowed to query domain verification agent");
+ }
+ final Computer snapshot = snapshotComputer();
+ return getDomainVerificationAgentComponentNameLPr(snapshot);
+ }
+
+ @Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
try {
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index e329f09..89589ed 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -396,6 +396,8 @@
return runArchive();
case "request-unarchive":
return runUnarchive();
+ case "get-domain-verification-agent":
+ return runGetDomainVerificationAgent();
default: {
if (ART_SERVICE_COMMANDS.contains(cmd)) {
if (DexOptHelper.useArtService()) {
@@ -4794,6 +4796,19 @@
return 0;
}
+ private int runGetDomainVerificationAgent() throws RemoteException {
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ final ComponentName domainVerificationAgent = mInterface.getDomainVerificationAgent();
+ pw.println(domainVerificationAgent == null
+ ? "No Domain Verifier available!" : domainVerificationAgent.toString());
+ } catch (Exception e) {
+ pw.println("Failure [" + e.getMessage() + "]");
+ return 1;
+ }
+ return 0;
+ }
+
@Override
public void onHelp() {
final PrintWriter pw = getOutPrintWriter();
@@ -5194,6 +5209,9 @@
pw.println(" to unarchive an app to the responsible installer. Options are:");
pw.println(" --user: request unarchival of the app from the given user.");
pw.println("");
+ pw.println(" get-domain-verification-agent");
+ pw.println(" Displays the component name of the domain verification agent on device.");
+ pw.println("");
if (DexOptHelper.useArtService()) {
printArtServiceHelp();
} else {
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index b720304..067a012 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -288,29 +288,6 @@
* configuration.
*/
private static UserTypeDetails.Builder getDefaultTypeProfilePrivate() {
- UserProperties.Builder userPropertiesBuilder = new UserProperties.Builder()
- .setStartWithParent(true)
- .setCredentialShareableWithParent(true)
- .setAuthAlwaysRequiredToDisableQuietMode(true)
- .setAllowStoppingUserWithDelayedLocking(true)
- .setMediaSharedWithParent(false)
- .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
- .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
- .setShowInQuietMode(
- UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
- .setShowInSharingSurfaces(
- UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
- .setCrossProfileIntentFilterAccessControl(
- UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM)
- .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT)
- .setCrossProfileContentSharingStrategy(
- UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT)
- .setItemsRestrictedOnHomeScreen(true);
- if (android.multiuser.Flags.supportHidingProfiles()) {
- userPropertiesBuilder.setProfileApiVisibility(
- UserProperties.PROFILE_API_VISIBILITY_HIDDEN);
- }
-
return new UserTypeDetails.Builder()
.setName(USER_TYPE_PROFILE_PRIVATE)
.setBaseType(FLAG_PROFILE)
@@ -329,7 +306,26 @@
.setDarkThemeBadgeColors(
R.color.white)
.setDefaultRestrictions(getDefaultProfileRestrictions())
- .setDefaultUserProperties(userPropertiesBuilder);
+ .setDefaultUserProperties(new UserProperties.Builder()
+ .setStartWithParent(true)
+ .setCredentialShareableWithParent(true)
+ .setAuthAlwaysRequiredToDisableQuietMode(true)
+ .setAllowStoppingUserWithDelayedLocking(true)
+ .setMediaSharedWithParent(false)
+ .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
+ .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
+ .setShowInQuietMode(
+ UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
+ .setShowInSharingSurfaces(
+ UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
+ .setCrossProfileIntentFilterAccessControl(
+ UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM)
+ .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT)
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT)
+ .setProfileApiVisibility(
+ UserProperties.PROFILE_API_VISIBILITY_HIDDEN)
+ .setItemsRestrictedOnHomeScreen(true));
}
/**
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
index 53ee189..7ca449a 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
@@ -26,6 +26,7 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
+import android.content.pm.verify.domain.DomainSet;
import android.content.pm.verify.domain.DomainVerificationInfo;
import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.DomainVerificationState;
@@ -230,13 +231,20 @@
* broadcast will be sent to the domain verification agent so it may re-run any verification
* logic for the newly associated domains.
* <p>
- * This will mutate internal {@link DomainVerificationPkgState} and so will hold the internal
- * lock. This should never be called from within the domain verification classes themselves.
+ * Optionally, the caller can specify a set of domains that are already pre-verified by the
+ * installer. These domains, if specified with autoVerify in the manifest, will be regarded as
+ * verified as soon as the app is installed, until the domain verification agent sends back the
+ * real verification results.
+ * <p>
+ * This method will mutate internal {@link DomainVerificationPkgState} and so will hold the
+ * internal lock. This should never be called from within the domain verification classes
+ * themselves.
* <p>
* This will NOT call {@link #writeSettings(Computer, TypedXmlSerializer, boolean, int)}. That must be
* handled by the caller.
*/
- void addPackage(@NonNull PackageStateInternal newPkgSetting);
+ void addPackage(@NonNull PackageStateInternal newPkgSetting,
+ @Nullable DomainSet preVerifiedDomains);
/**
* Migrates verification state from a previous install to a new one. It is expected that the
@@ -245,14 +253,20 @@
* domains under the assumption that the new package will pass the same server side config as
* the previous package, as they have matching signatures.
* <p>
- * This will mutate internal {@link DomainVerificationPkgState} and so will hold the internal
- * lock. This should never be called from within the domain verification classes themselves.
+ * Optionally, the caller can specify a set of domains that are already pre-verified by the
+ * installer. These domains, if specified with autoVerify in the manifest, will be regarded as
+ * verified as soon as the app is updated, until the domain verification agent sends back the
+ * real verification results.
+ * <p>
+ * This method will mutate internal {@link DomainVerificationPkgState} and so will hold the
+ * internal lock. This should never be called from within the domain verification classes
+ * themselves.
* <p>
* This will NOT call {@link #writeSettings(Computer, TypedXmlSerializer, boolean, int)}. That must be
* handled by the caller.
*/
void migrateState(@NonNull PackageStateInternal oldPkgSetting,
- @NonNull PackageStateInternal newPkgSetting);
+ @NonNull PackageStateInternal newPkgSetting, @Nullable DomainSet preVerifiedDomains);
/**
* Serializes the entire internal state. This is equivalent to a full backup of the existing
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
index 6150099..c796b40 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
@@ -32,6 +32,7 @@
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.verify.domain.DomainOwner;
+import android.content.pm.verify.domain.DomainSet;
import android.content.pm.verify.domain.DomainVerificationInfo;
import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.DomainVerificationState;
@@ -859,7 +860,7 @@
@Override
public void migrateState(@NonNull PackageStateInternal oldPkgSetting,
- @NonNull PackageStateInternal newPkgSetting) {
+ @NonNull PackageStateInternal newPkgSetting, @Nullable DomainSet preVerifiedDomains) {
String pkgName = newPkgSetting.getPackageName();
boolean sendBroadcast;
@@ -935,6 +936,9 @@
sendBroadcast = hasAutoVerifyDomains && needsBroadcast;
+ // Apply pre-verified states as the last step of migration
+ applyPreVerifiedState(newStateMap, newAutoVerifyDomains, preVerifiedDomains);
+
mAttachedPkgStates.put(pkgName, newDomainSetId, new DomainVerificationPkgState(
pkgName, newDomainSetId, hasAutoVerifyDomains, newStateMap, newUserStates,
null /* signature */));
@@ -947,7 +951,8 @@
// TODO(b/159952358): Handle valid domainSetIds for PackageStateInternals with no AndroidPackage
@Override
- public void addPackage(@NonNull PackageStateInternal newPkgSetting) {
+ public void addPackage(@NonNull PackageStateInternal newPkgSetting,
+ @Nullable DomainSet preVerifiedDomains) {
// TODO(b/159952358): Optimize packages without any domains. Those wouldn't have to be in
// the state map, but it would require handling the "migration" case where an app either
// gains or loses all domains.
@@ -1029,6 +1034,9 @@
DomainVerificationState.STATE_MIGRATED);
}
}
+
+ // Apply pre-verified states before sending out broadcast
+ applyPreVerifiedState(pkgState.getStateMap(), autoVerifyDomains, preVerifiedDomains);
}
synchronized (mLock) {
@@ -1040,6 +1048,27 @@
}
}
+ private void applyPreVerifiedState(ArrayMap<String, Integer> stateMap,
+ ArraySet<String> autoVerifyDomains,
+ DomainSet preVerifiedDomains) {
+ // If any pre-verified domains are provided, treating them as verified as well. This
+ // allows the app to be opened immediately by the corresponding app links, but the
+ // pre-verified state can still be overwritten by the domain verification agent in the
+ // future.
+ if (preVerifiedDomains != null && !autoVerifyDomains.isEmpty()) {
+ for (String preVerifiedDomain : preVerifiedDomains.getDomains()) {
+ if (autoVerifyDomains.contains(preVerifiedDomain)
+ && !stateMap.containsKey(preVerifiedDomain)) {
+ // Only set the pre-verified state if there's no existing state
+ stateMap.put(preVerifiedDomain, DomainVerificationState.STATE_PRE_VERIFIED);
+ if (DEBUG_APPROVAL) {
+ Slog.d(TAG, "Inserted pre-verified domain: " + preVerifiedDomain);
+ }
+ }
+ }
+ }
+ }
+
/**
* Applies any immutable state as the final step when adding or migrating state. Currently only
* applies {@link SystemConfig#getLinkedApps()}, which approves all domains for a system app.
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java
index 466c4c9..13b072b 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationShell.java
@@ -62,6 +62,7 @@
pw.println(" - restored: preserved verification from a user data restore");
pw.println(" - legacy_failure: rejected by a legacy verifier, unknown reason");
pw.println(" - system_configured: automatically approved by the device config");
+ pw.println(" - pre_verified: the domain was pre-verified by the installer");
pw.println(" - >= 1024: Custom error code which is specific to the device verifier");
pw.println(" --user <USER_ID>: include user selections (includes all domains, not");
pw.println(" just autoVerify ones)");
diff --git a/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java b/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
index b2bbcda..e1abae8 100644
--- a/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
+++ b/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
@@ -32,6 +32,8 @@
import com.android.internal.infra.ServiceConnector;
+import java.io.IOException;
+
/** Manages the connection to the remote wearable sensing service. */
final class RemoteWearableSensingService extends ServiceConnector.Impl<IWearableSensingService> {
private static final String TAG =
@@ -56,6 +58,29 @@
}
/**
+ * Provides a secure connection to the wearable.
+ *
+ * @param secureWearableConnection The secure connection to the wearable
+ * @param callback The callback for service status
+ */
+ public void provideSecureWearableConnection(
+ ParcelFileDescriptor secureWearableConnection, RemoteCallback callback) {
+ if (DEBUG) {
+ Slog.i(TAG, "Providing secure wearable connection.");
+ }
+ var unused = post(
+ service -> {
+ service.provideSecureWearableConnection(secureWearableConnection, callback);
+ try {
+ // close the local fd after it has been sent to the WSS process
+ secureWearableConnection.close();
+ } catch (IOException ex) {
+ Slog.w(TAG, "Unable to close the local parcelFileDescriptor.", ex);
+ }
+ });
+ }
+
+ /**
* Provides the implementation a data stream to the wearable.
*
* @param parcelFileDescriptor The data stream to the wearable
@@ -66,7 +91,16 @@
if (DEBUG) {
Slog.i(TAG, "Providing data stream.");
}
- post(service -> service.provideDataStream(parcelFileDescriptor, callback));
+ var unused = post(
+ service -> {
+ service.provideDataStream(parcelFileDescriptor, callback);
+ try {
+ // close the local fd after it has been sent to the WSS process
+ parcelFileDescriptor.close();
+ } catch (IOException ex) {
+ Slog.w(TAG, "Unable to close the local parcelFileDescriptor.", ex);
+ }
+ });
}
/**
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
index e73fd0f..a8d6322 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
@@ -22,17 +22,19 @@
import android.annotation.UserIdInt;
import android.app.AppGlobals;
import android.app.ambientcontext.AmbientContextEvent;
+import android.app.wearable.Flags;
import android.app.wearable.WearableSensingManager;
+import android.companion.CompanionDeviceManager;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Bundle;
-import android.system.OsConstants;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.SharedMemory;
+import android.system.OsConstants;
import android.util.IndentingPrintWriter;
import android.util.Slog;
@@ -40,6 +42,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.infra.AbstractPerUserSystemService;
+import java.io.IOException;
import java.io.PrintWriter;
/**
@@ -55,6 +58,10 @@
RemoteWearableSensingService mRemoteService;
private ComponentName mComponentName;
+ private final Object mSecureChannelLock = new Object();
+
+ @GuardedBy("mSecureChannelLock")
+ private WearableSensingSecureChannel mSecureChannel;
WearableSensingManagerPerUserService(
@NonNull WearableSensingManagerService master, Object lock, @UserIdInt int userId) {
@@ -76,6 +83,11 @@
mRemoteService = null;
}
}
+ synchronized (mSecureChannelLock) {
+ if (mSecureChannel != null) {
+ mSecureChannel.close();
+ }
+ }
}
@GuardedBy("mLock")
@@ -156,6 +168,63 @@
}
/**
+ * Creates a CompanionDeviceManager secure channel and sends a proxy to the wearable sensing
+ * service.
+ */
+ public void onProvideWearableConnection(
+ ParcelFileDescriptor wearableConnection, RemoteCallback callback) {
+ Slog.i(TAG, "onProvideWearableConnection in per user service.");
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Detection service is not available at this moment.");
+ notifyStatusCallback(callback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ }
+ synchronized (mSecureChannelLock) {
+ if (mSecureChannel != null) {
+ // TODO(b/321012559): Kill the WearableSensingService process if it has not been
+ // killed from onError
+ mSecureChannel.close();
+ }
+ try {
+ mSecureChannel =
+ WearableSensingSecureChannel.create(
+ getContext().getSystemService(CompanionDeviceManager.class),
+ wearableConnection,
+ new WearableSensingSecureChannel.SecureTransportListener() {
+ @Override
+ public void onSecureTransportAvailable(
+ ParcelFileDescriptor secureTransport) {
+ Slog.i(TAG, "calling over to remote service.");
+ synchronized (mLock) {
+ ensureRemoteServiceInitiated();
+ mRemoteService.provideSecureWearableConnection(
+ secureTransport, callback);
+ }
+ }
+
+ @Override
+ public void onError() {
+ // TODO(b/321012559): Kill the WearableSensingService
+ // process if mSecureChannel has not been reassigned
+ if (Flags.enableProvideWearableConnectionApi()) {
+ notifyStatusCallback(
+ callback,
+ WearableSensingManager.STATUS_CHANNEL_ERROR);
+ }
+ }
+ });
+ } catch (IOException ex) {
+ Slog.e(TAG, "Unable to create the secure channel.", ex);
+ if (Flags.enableProvideWearableConnectionApi()) {
+ notifyStatusCallback(callback, WearableSensingManager.STATUS_CHANNEL_ERROR);
+ }
+ }
+ }
+ }
+
+ /**
* Handles sending the provided data stream for the wearable to the wearable sensing service.
*/
public void onProvideDataStream(
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index 4cc2c02..28c8f87 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -211,9 +211,27 @@
private final class WearableSensingManagerInternal extends IWearableSensingManager.Stub {
@Override
+ public void provideWearableConnection(
+ ParcelFileDescriptor wearableConnection, RemoteCallback callback) {
+ Slog.i(TAG, "WearableSensingManagerInternal provideWearableConnection.");
+ Objects.requireNonNull(wearableConnection);
+ Objects.requireNonNull(callback);
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG);
+ if (!mIsServiceEnabled) {
+ Slog.w(TAG, "Service not available.");
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ callback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ callPerUserServiceIfExist(
+ service -> service.onProvideWearableConnection(wearableConnection, callback),
+ callback);
+ }
+
+ @Override
public void provideDataStream(
- ParcelFileDescriptor parcelFileDescriptor,
- RemoteCallback callback) {
+ ParcelFileDescriptor parcelFileDescriptor, RemoteCallback callback) {
Slog.i(TAG, "WearableSensingManagerInternal provideDataStream.");
Objects.requireNonNull(parcelFileDescriptor);
Objects.requireNonNull(callback);
diff --git a/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java b/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java
new file mode 100644
index 0000000..a16ff51
--- /dev/null
+++ b/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java
@@ -0,0 +1,350 @@
+/*
+ * 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.server.wearable;
+
+import android.annotation.NonNull;
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
+import android.companion.CompanionDeviceManager;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.AutoCloseInputStream;
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * A wrapper that manages a CompanionDeviceManager secure channel for wearable sensing.
+ *
+ * <p>This wrapper accepts a connection to a wearable from the caller. It then attaches the
+ * connection to the CompanionDeviceManager via {@link
+ * CompanionDeviceManager#attachSystemDataTransport(int, InputStream, OutputStream)}, which will
+ * create an encrypted channel using the provided connection as the raw underlying connection. The
+ * wearable device is expected to attach its side of the raw connection to its
+ * CompanionDeviceManager via the same method so that the two CompanionDeviceManagers on the two
+ * devices can perform attestation and set up the encrypted channel. Attestation requirements are
+ * listed in {@link com.android.server.security.AttestationVerificationPeerDeviceVerifier}.
+ *
+ * <p>When the encrypted channel is available, it will be provided to the caller via the
+ * SecureTransportListener.
+ */
+final class WearableSensingSecureChannel {
+
+ /** A listener for secure transport and its error signal. */
+ interface SecureTransportListener {
+
+ /** Called when the secure transport is available. */
+ void onSecureTransportAvailable(ParcelFileDescriptor secureTransport);
+
+ /**
+ * Called when there is a non-recoverable error. The secure channel will be automatically
+ * closed.
+ */
+ void onError();
+ }
+
+ private static final String TAG = WearableSensingSecureChannel.class.getSimpleName();
+ private static final String CDM_ASSOCIATION_DISPLAY_NAME = "PlaceholderDisplayNameFromWSM";
+ // The batch size of reading from the ParcelFileDescriptor returned to mSecureTransportListener
+ private static final int READ_BUFFER_SIZE = 8192;
+
+ private final Object mLock = new Object();
+ // CompanionDeviceManager (CDM) can continue to call these ExecutorServices even after the
+ // corresponding cleanup methods in CDM have been called (e.g.
+ // removeOnTransportsChangedListener). Since we shut down these ExecutorServices after
+ // clean up, we use SoftShutdownExecutor to suppress RejectedExecutionExceptions.
+ private final SoftShutdownExecutor mMessageFromWearableExecutor =
+ new SoftShutdownExecutor(Executors.newSingleThreadExecutor());
+ private final SoftShutdownExecutor mMessageToWearableExecutor =
+ new SoftShutdownExecutor(Executors.newSingleThreadExecutor());
+ private final SoftShutdownExecutor mLightWeightExecutor =
+ new SoftShutdownExecutor(Executors.newSingleThreadExecutor());
+ private final CompanionDeviceManager mCompanionDeviceManager;
+ private final ParcelFileDescriptor mUnderlyingTransport;
+ private final SecureTransportListener mSecureTransportListener;
+ private final AtomicBoolean mTransportAvailable = new AtomicBoolean(false);
+ private final Consumer<List<AssociationInfo>> mOnTransportsChangedListener =
+ this::onTransportsChanged;
+ private final BiConsumer<Integer, byte[]> mOnMessageReceivedListener = this::onMessageReceived;
+ private final ParcelFileDescriptor mRemoteFd; // To be returned to mSecureTransportListener
+ // read input received from the ParcelFileDescriptor returned to mSecureTransportListener
+ private final InputStream mLocalIn;
+ // send output to the ParcelFileDescriptor returned to mSecureTransportListener
+ private final OutputStream mLocalOut;
+
+ @GuardedBy("mLock")
+ private boolean mClosed = false;
+
+ private Integer mAssociationId = null;
+
+ /**
+ * Creates a WearableSensingSecureChannel. When the secure transport is ready,
+ * secureTransportListener will be notified.
+ *
+ * @param companionDeviceManager The CompanionDeviceManager system service.
+ * @param underlyingTransport The underlying transport to create the secure channel on.
+ * @param secureTransportListener The listener to receive the secure transport when it is ready.
+ * @throws IOException if it cannot create a {@link ParcelFileDescriptor} socket pair.
+ */
+ static WearableSensingSecureChannel create(
+ @NonNull CompanionDeviceManager companionDeviceManager,
+ @NonNull ParcelFileDescriptor underlyingTransport,
+ @NonNull SecureTransportListener secureTransportListener)
+ throws IOException {
+ Objects.requireNonNull(companionDeviceManager);
+ Objects.requireNonNull(underlyingTransport);
+ Objects.requireNonNull(secureTransportListener);
+ ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair();
+ WearableSensingSecureChannel channel =
+ new WearableSensingSecureChannel(
+ companionDeviceManager,
+ underlyingTransport,
+ secureTransportListener,
+ pair[0],
+ pair[1]);
+ channel.initialize();
+ return channel;
+ }
+
+ private WearableSensingSecureChannel(
+ CompanionDeviceManager companionDeviceManager,
+ ParcelFileDescriptor underlyingTransport,
+ SecureTransportListener secureTransportListener,
+ ParcelFileDescriptor remoteFd,
+ ParcelFileDescriptor localFd) {
+ mCompanionDeviceManager = companionDeviceManager;
+ mUnderlyingTransport = underlyingTransport;
+ mSecureTransportListener = secureTransportListener;
+ mRemoteFd = remoteFd;
+ mLocalIn = new AutoCloseInputStream(localFd);
+ mLocalOut = new AutoCloseOutputStream(localFd);
+ }
+
+ private void initialize() {
+ final long originalCallingIdentity = Binder.clearCallingIdentity();
+ try {
+ Slog.d(TAG, "Requesting CDM association.");
+ mCompanionDeviceManager.associate(
+ new AssociationRequest.Builder()
+ .setDisplayName(CDM_ASSOCIATION_DISPLAY_NAME)
+ .setSelfManaged(true)
+ .build(),
+ mLightWeightExecutor,
+ new CompanionDeviceManager.Callback() {
+ @Override
+ public void onAssociationCreated(AssociationInfo associationInfo) {
+ WearableSensingSecureChannel.this.onAssociationCreated(
+ associationInfo.getId());
+ }
+
+ @Override
+ public void onFailure(CharSequence error) {
+ Slog.e(
+ TAG,
+ "Failed to create CompanionDeviceManager association: "
+ + error);
+ onError();
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(originalCallingIdentity);
+ }
+ }
+
+ private void onAssociationCreated(int associationId) {
+ Slog.i(TAG, "CDM association created.");
+ synchronized (mLock) {
+ if (mClosed) {
+ return;
+ }
+ mAssociationId = associationId;
+ mCompanionDeviceManager.addOnMessageReceivedListener(
+ mMessageFromWearableExecutor,
+ CompanionDeviceManager.MESSAGE_ONEWAY_FROM_WEARABLE,
+ mOnMessageReceivedListener);
+ mCompanionDeviceManager.addOnTransportsChangedListener(
+ mLightWeightExecutor, mOnTransportsChangedListener);
+ mCompanionDeviceManager.attachSystemDataTransport(
+ associationId,
+ new AutoCloseInputStream(mUnderlyingTransport),
+ new AutoCloseOutputStream(mUnderlyingTransport));
+ }
+ }
+
+ private void onTransportsChanged(List<AssociationInfo> associationInfos) {
+ synchronized (mLock) {
+ if (mClosed) {
+ return;
+ }
+ if (mAssociationId == null) {
+ Slog.e(TAG, "mAssociationId is null when transport changed");
+ return;
+ }
+ }
+ // Do not call onTransportAvailable() or onError() when holding the lock because it can
+ // cause a deadlock if the callback holds another lock.
+ boolean transportAvailable =
+ associationInfos.stream().anyMatch(info -> info.getId() == mAssociationId);
+ if (transportAvailable && mTransportAvailable.compareAndSet(false, true)) {
+ onTransportAvailable();
+ } else if (!transportAvailable && mTransportAvailable.compareAndSet(true, false)) {
+ Slog.i(TAG, "CDM transport is detached. This is not recoverable.");
+ onError();
+ }
+ }
+
+ private void onTransportAvailable() {
+ // Start sending data received from the remote stream to the wearable.
+ Slog.i(TAG, "Transport available");
+ mMessageToWearableExecutor.execute(
+ () -> {
+ int[] associationIdsToSendMessageTo = new int[] {mAssociationId};
+ byte[] buffer = new byte[READ_BUFFER_SIZE];
+ int readLen;
+ try {
+ while ((readLen = mLocalIn.read(buffer)) != -1) {
+ byte[] data = new byte[readLen];
+ System.arraycopy(buffer, 0, data, 0, readLen);
+ Slog.v(TAG, "Sending message to wearable");
+ mCompanionDeviceManager.sendMessage(
+ CompanionDeviceManager.MESSAGE_ONEWAY_TO_WEARABLE,
+ data,
+ associationIdsToSendMessageTo);
+ }
+ } catch (IOException e) {
+ Slog.i(TAG, "IOException while reading from remote stream.");
+ onError();
+ return;
+ }
+ Slog.i(
+ TAG,
+ "Reached EOF when reading from remote stream. Reporting this as an"
+ + " error.");
+ onError();
+ });
+ mSecureTransportListener.onSecureTransportAvailable(mRemoteFd);
+ }
+
+ private void onMessageReceived(int associationIdForMessage, byte[] data) {
+ if (associationIdForMessage == mAssociationId) {
+ Slog.v(TAG, "Received message from wearable.");
+ try {
+ mLocalOut.write(data);
+ mLocalOut.flush();
+ } catch (IOException e) {
+ Slog.i(
+ TAG,
+ "IOException when writing to remote stream. Closing the secure channel.");
+ onError();
+ }
+ } else {
+ Slog.v(
+ TAG,
+ "Received CDM message of type MESSAGE_ONEWAY_FROM_WEARABLE, but it is for"
+ + " another association. Ignoring the message.");
+ }
+ }
+
+ private void onError() {
+ synchronized (mLock) {
+ if (mClosed) {
+ return;
+ }
+ }
+ mSecureTransportListener.onError();
+ close();
+ }
+
+ /** Closes this secure channel and releases all resources. */
+ void close() {
+ synchronized (mLock) {
+ if (mClosed) {
+ return;
+ }
+ Slog.i(TAG, "Closing WearableSensingSecureChannel.");
+ mClosed = true;
+ if (mAssociationId != null) {
+ final long originalCallingIdentity = Binder.clearCallingIdentity();
+ try {
+ mCompanionDeviceManager.removeOnTransportsChangedListener(
+ mOnTransportsChangedListener);
+ mCompanionDeviceManager.removeOnMessageReceivedListener(
+ CompanionDeviceManager.MESSAGE_ONEWAY_FROM_WEARABLE,
+ mOnMessageReceivedListener);
+ mCompanionDeviceManager.detachSystemDataTransport(mAssociationId);
+ mCompanionDeviceManager.disassociate(mAssociationId);
+ } finally {
+ Binder.restoreCallingIdentity(originalCallingIdentity);
+ }
+ }
+ try {
+ mLocalIn.close();
+ } catch (IOException ex) {
+ Slog.e(TAG, "Encountered IOException when closing local input stream.", ex);
+ }
+ try {
+ mLocalOut.close();
+ } catch (IOException ex) {
+ Slog.e(TAG, "Encountered IOException when closing local output stream.", ex);
+ }
+ mMessageFromWearableExecutor.shutdown();
+ mMessageToWearableExecutor.shutdown();
+ mLightWeightExecutor.shutdown();
+ }
+ }
+
+ /**
+ * An executor that can be shutdown. Unlike an ExecutorService, it will not throw a
+ * RejectedExecutionException if {@link #execute(Runnable)} is called after shutdown.
+ */
+ private static class SoftShutdownExecutor implements Executor {
+
+ private final ExecutorService mExecutorService;
+
+ SoftShutdownExecutor(ExecutorService executorService) {
+ mExecutorService = executorService;
+ }
+
+ @Override
+ public void execute(Runnable runnable) {
+ try {
+ mExecutorService.execute(runnable);
+ } catch (RejectedExecutionException ex) {
+ Slog.d(TAG, "Received new runnable after shutdown. Ignoring.");
+ }
+ }
+
+ /** Shutdown the underlying ExecutorService. */
+ void shutdown() {
+ mExecutorService.shutdown();
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 7b59759..c2117ea 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2441,11 +2441,11 @@
return false;
}
- if (mStartingData != null) {
+ if (hasStartingWindow()) {
return false;
}
- final WindowState mainWin = findMainWindow();
+ final WindowState mainWin = findMainWindow(false /* includeStartingApp */);
if (mainWin != null && mainWin.mWinAnimator.getShown()) {
// App already has a visible window...why would you want a starting window?
return false;
diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java
index 4ced5d5..f2dc55f 100644
--- a/services/core/java/com/android/server/wm/SafeActivityOptions.java
+++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java
@@ -333,7 +333,9 @@
if (aInfo != null && overrideTaskTransition) {
final int startTasksFromRecentsPerm = ActivityTaskManagerService.checkPermission(
START_TASKS_FROM_RECENTS, callingPid, callingUid);
- if (startTasksFromRecentsPerm != PERMISSION_GRANTED) {
+ // Allow if calling uid is from assistant, or start task from recents
+ if (startTasksFromRecentsPerm != PERMISSION_GRANTED
+ && !isAssistant(supervisor.mService, callingUid)) {
final String msg = "Permission Denial: starting " + getIntentString(intent)
+ " from " + callerApp + " (pid=" + callingPid
+ ", uid=" + callingUid + ") with overrideTaskTransition=true";
diff --git a/services/core/java/com/android/server/wm/WindowList.java b/services/core/java/com/android/server/wm/WindowList.java
index dfeba40..1e888f5 100644
--- a/services/core/java/com/android/server/wm/WindowList.java
+++ b/services/core/java/com/android/server/wm/WindowList.java
@@ -24,7 +24,7 @@
*/
class WindowList<E> extends ArrayList<E> {
- void addFirst(E e) {
+ public void addFirst(E e) {
add(0, e);
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 57448cb..366e1bb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1114,8 +1114,11 @@
public static WindowManagerService main(final Context context, final InputManagerService im,
final boolean showBootMsgs, WindowManagerPolicy policy,
ActivityTaskManagerService atm) {
- return main(context, im, showBootMsgs, policy, atm, new DisplayWindowSettingsProvider(),
- SurfaceControl.Transaction::new, SurfaceControl.Builder::new);
+ final WindowManagerService wms = main(context, im, showBootMsgs, policy, atm,
+ new DisplayWindowSettingsProvider(), SurfaceControl.Transaction::new,
+ SurfaceControl.Builder::new);
+ WindowManagerGlobal.setWindowManagerServiceForSystemProcess(wms);
+ return wms;
}
/**
diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
index b1349ea..f5ba50d 100644
--- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
@@ -27,6 +27,7 @@
import android.credentials.selection.RequestInfo;
import android.os.CancellationSignal;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.service.credentials.CallingAppInfo;
import android.util.Slog;
@@ -149,7 +150,7 @@
}
@Override
- public void onUiCancellation(boolean isUserCancellation) {
+ public void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver) {
// Not needed since UI is not involved
}
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index b6f7eb3..be4b9e1 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -31,6 +31,7 @@
import android.credentials.selection.RequestInfo;
import android.os.CancellationSignal;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.PermissionUtils;
import android.util.Slog;
@@ -163,7 +164,7 @@
}
@Override
- public void onUiCancellation(boolean isUserCancellation) {
+ public void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver) {
String exception = CreateCredentialException.TYPE_USER_CANCELED;
String message = "User cancelled the selector";
if (!isUserCancellation) {
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
index 84b5cb7..534c842 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
@@ -15,6 +15,8 @@
*/
package com.android.server.credentials;
+import static android.credentials.selection.Constants.EXTRA_FINAL_RESPONSE_RECEIVER;
+
import android.annotation.NonNull;
import android.app.PendingIntent;
import android.content.ComponentName;
@@ -34,7 +36,6 @@
import android.os.ResultReceiver;
import android.os.UserHandle;
import android.service.credentials.CredentialProviderInfoFactory;
-import android.util.Slog;
import java.util.ArrayList;
import java.util.HashSet;
@@ -79,20 +80,25 @@
UserSelectionDialogResult selection = UserSelectionDialogResult
.fromResultData(resultData);
if (selection != null) {
- mCallbacks.onUiSelection(selection);
- } else {
- Slog.i(TAG, "No selection found in UI result");
+ ResultReceiver resultReceiver = resultData.getParcelable(
+ EXTRA_FINAL_RESPONSE_RECEIVER,
+ ResultReceiver.class);
+ mCallbacks.onUiSelection(selection, resultReceiver);
}
break;
case UserSelectionDialogResult.RESULT_CODE_DIALOG_USER_CANCELED:
mStatus = UiStatus.TERMINATED;
- mCallbacks.onUiCancellation(/* isUserCancellation= */ true);
+ mCallbacks.onUiCancellation(/* isUserCancellation= */ true,
+ resultData.getParcelable(EXTRA_FINAL_RESPONSE_RECEIVER,
+ ResultReceiver.class));
break;
case UserSelectionDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS:
mStatus = UiStatus.TERMINATED;
- mCallbacks.onUiCancellation(/* isUserCancellation= */ false);
+ mCallbacks.onUiCancellation(/* isUserCancellation= */ false,
+ resultData.getParcelable(EXTRA_FINAL_RESPONSE_RECEIVER,
+ ResultReceiver.class));
break;
case UserSelectionDialogResult.RESULT_CODE_DATA_PARSING_FAILURE:
mStatus = UiStatus.TERMINATED;
@@ -116,10 +122,10 @@
*/
public interface CredentialManagerUiCallback {
/** Called when the user makes a selection. */
- void onUiSelection(UserSelectionDialogResult selection);
+ void onUiSelection(UserSelectionDialogResult selection, ResultReceiver resultReceiver);
/** Called when the UI is canceled without a successful provider result. */
- void onUiCancellation(boolean isUserCancellation);
+ void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver);
/** Called when the selector UI fails to come up (mostly due to parsing issue today). */
void onUiSelectorInvocationFailure();
diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
index 9e362b3..adb1b72 100644
--- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
@@ -20,6 +20,7 @@
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
+import android.credentials.Constants;
import android.credentials.CredentialProviderInfo;
import android.credentials.GetCandidateCredentialsException;
import android.credentials.GetCandidateCredentialsResponse;
@@ -29,10 +30,13 @@
import android.credentials.selection.GetCredentialProviderData;
import android.credentials.selection.ProviderData;
import android.credentials.selection.RequestInfo;
+import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.service.credentials.CallingAppInfo;
+import android.service.credentials.CredentialProviderService;
import android.service.credentials.PermissionUtils;
import android.util.Slog;
@@ -153,7 +157,8 @@
}
@Override
- public void onUiCancellation(boolean isUserCancellation) {
+ public void onUiCancellation(boolean isUserCancellation,
+ @Nullable ResultReceiver finalResponseReceiver) {
String exception = GetCandidateCredentialsException.TYPE_USER_CANCELED;
String message = "User cancelled the selector";
if (!isUserCancellation) {
@@ -161,7 +166,12 @@
message = "The UI was interrupted - please try again.";
}
mRequestSessionMetric.collectFrameworkException(exception);
- respondToClientWithErrorAndFinish(exception, message);
+ if (finalResponseReceiver != null) {
+ Bundle resultData = new Bundle();
+ finalResponseReceiver.send(Constants.FAILURE_CREDMAN_SELECTOR, resultData);
+ } else {
+ respondToClientWithErrorAndFinish(exception, message);
+ }
}
@Override
@@ -197,7 +207,16 @@
public void onFinalResponseReceived(ComponentName componentName,
GetCredentialResponse response) {
Slog.d(TAG, "onFinalResponseReceived");
- respondToClientWithResponseAndFinish(new GetCandidateCredentialsResponse(response));
+ if (this.mFinalResponseReceiver != null) {
+ Slog.d(TAG, "onFinalResponseReceived sending through final receiver");
+ Bundle resultData = new Bundle();
+ resultData.putParcelable(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE, response);
+ mFinalResponseReceiver.send(Constants.SUCCESS_CREDMAN_SELECTOR, resultData);
+ finishSession(/*propagateCancellation=*/ false);
+ } else {
+ Slog.w(TAG, "onFinalResponseReceived result receiver not found for pinned entry");
+ }
}
/**
@@ -212,6 +231,5 @@
*/
public int getAutofillRequestId() {
return mAutofillRequestId;
-
}
}
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index 4068d7b..a279337 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -31,6 +31,7 @@
import android.os.Binder;
import android.os.CancellationSignal;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.PermissionUtils;
import android.util.Slog;
@@ -165,7 +166,7 @@
}
@Override
- public void onUiCancellation(boolean isUserCancellation) {
+ public void onUiCancellation(boolean isUserCancellation, ResultReceiver resultReceiver) {
String exception = GetCredentialException.TYPE_USER_CANCELED;
String message = "User cancelled the selector";
if (!isUserCancellation) {
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index bf7df86..633c9c4 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -17,6 +17,7 @@
package com.android.server.credentials;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.content.ComponentName;
@@ -33,6 +34,7 @@
import android.os.IInterface;
import android.os.Looper;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.os.UserHandle;
import android.service.credentials.CallingAppInfo;
import android.util.Slog;
@@ -101,6 +103,9 @@
protected PendingIntent mPendingIntent;
+ @Nullable
+ protected ResultReceiver mFinalResponseReceiver;
+
@NonNull
protected RequestSessionStatus mRequestSessionStatus =
RequestSessionStatus.IN_PROGRESS;
@@ -219,7 +224,8 @@
// UI callbacks
@Override // from CredentialManagerUiCallbacks
- public void onUiSelection(UserSelectionDialogResult selection) {
+ public void onUiSelection(UserSelectionDialogResult selection,
+ ResultReceiver finalResponseReceiver) {
if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) {
Slog.w(TAG, "Request has already been completed. This is strange.");
return;
@@ -234,6 +240,7 @@
Slog.w(TAG, "providerSession not found in onUiSelection. This is strange.");
return;
}
+ mFinalResponseReceiver = finalResponseReceiver;
ProviderSessionMetric providerSessionMetric = providerSession.mProviderSessionMetric;
int initialAuthMetricsProvider = providerSessionMetric.getBrowsedAuthenticationMetric()
.size();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 74d544f..6f0985a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -34,6 +34,7 @@
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CAMERA;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CERTIFICATES;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE;
+import static android.Manifest.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DEFAULT_SMS;
import static android.Manifest.permission.MANAGE_DEVICE_POLICY_DISPLAY;
@@ -110,6 +111,8 @@
import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE;
import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_USER;
import static android.app.admin.DevicePolicyManager.ACTION_SYSTEM_UPDATE_POLICY_CHANGED;
+import static android.app.admin.DevicePolicyManager.CONTENT_PROTECTION_DISABLED;
+import static android.app.admin.DevicePolicyManager.ContentProtectionPolicy;
import static android.app.admin.DevicePolicyManager.DELEGATION_APP_RESTRICTIONS;
import static android.app.admin.DevicePolicyManager.DELEGATION_BLOCK_UNINSTALL;
import static android.app.admin.DevicePolicyManager.DELEGATION_CERT_INSTALL;
@@ -220,6 +223,7 @@
import static android.app.admin.ProvisioningException.ERROR_SETTING_PROFILE_OWNER_FAILED;
import static android.app.admin.ProvisioningException.ERROR_SET_DEVICE_OWNER_FAILED;
import static android.app.admin.ProvisioningException.ERROR_STARTING_PROFILE_FAILED;
+import static android.app.admin.flags.Flags.backupServiceSecurityLogEventEnabled;
import static android.app.admin.flags.Flags.dumpsysPolicyEngineMigrationEnabled;
import static android.app.admin.flags.Flags.policyEngineMigrationV2Enabled;
import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE;
@@ -17926,6 +17930,13 @@
|| isProfileOwner(caller) || isFinancedDeviceOwner(caller));
toggleBackupServiceActive(caller.getUserId(), enabled);
+
+ if (backupServiceSecurityLogEventEnabled()) {
+ if (SecurityLog.isLoggingEnabled()) {
+ SecurityLog.writeEvent(SecurityLog.TAG_BACKUP_SERVICE_TOGGLED,
+ caller.getPackageName(), caller.getUserId(), enabled ? 1 : 0);
+ }
+ }
}
@Override
@@ -23165,6 +23176,90 @@
}
}
+ private EnforcingAdmin enforceCanCallContentProtectionLocked(
+ ComponentName who, String callerPackageName) {
+ CallerIdentity caller = getCallerIdentity(who, callerPackageName);
+ final int userId = caller.getUserId();
+
+ EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin(
+ who,
+ MANAGE_DEVICE_POLICY_CONTENT_PROTECTION,
+ caller.getPackageName(),
+ userId
+ );
+ if ((isDeviceOwner(caller) || isProfileOwner(caller))
+ && !canDPCManagedUserUseLockTaskLocked(userId)) {
+ throw new SecurityException(
+ "User " + userId + " is not allowed to use content protection");
+ }
+ return enforcingAdmin;
+ }
+
+ private void enforceCanQueryContentProtectionLocked(
+ ComponentName who, String callerPackageName) {
+ CallerIdentity caller = getCallerIdentity(who, callerPackageName);
+ final int userId = caller.getUserId();
+
+ enforceCanQuery(MANAGE_DEVICE_POLICY_CONTENT_PROTECTION, caller.getPackageName(), userId);
+ if ((isDeviceOwner(caller) || isProfileOwner(caller))
+ && !canDPCManagedUserUseLockTaskLocked(userId)) {
+ throw new SecurityException(
+ "User " + userId + " is not allowed to use content protection");
+ }
+ }
+
+ @Override
+ public void setContentProtectionPolicy(
+ ComponentName who, String callerPackageName, @ContentProtectionPolicy int policy)
+ throws SecurityException {
+ if (!android.view.contentprotection.flags.Flags.manageDevicePolicyEnabled()) {
+ return;
+ }
+
+ CallerIdentity caller = getCallerIdentity(who, callerPackageName);
+ checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_CONTENT_PROTECTION_POLICY);
+
+ EnforcingAdmin enforcingAdmin;
+ synchronized (getLockObject()) {
+ enforcingAdmin = enforceCanCallContentProtectionLocked(who, caller.getPackageName());
+ }
+
+ if (policy == CONTENT_PROTECTION_DISABLED) {
+ mDevicePolicyEngine.removeLocalPolicy(
+ PolicyDefinition.CONTENT_PROTECTION,
+ enforcingAdmin,
+ caller.getUserId());
+ } else {
+ mDevicePolicyEngine.setLocalPolicy(
+ PolicyDefinition.CONTENT_PROTECTION,
+ enforcingAdmin,
+ new IntegerPolicyValue(policy),
+ caller.getUserId());
+ }
+ }
+
+ @Override
+ public @ContentProtectionPolicy int getContentProtectionPolicy(
+ ComponentName who, String callerPackageName) {
+ if (!android.view.contentprotection.flags.Flags.manageDevicePolicyEnabled()) {
+ return CONTENT_PROTECTION_DISABLED;
+ }
+
+ CallerIdentity caller = getCallerIdentity(who, callerPackageName);
+ final int userHandle = caller.getUserId();
+
+ synchronized (getLockObject()) {
+ enforceCanQueryContentProtectionLocked(who, caller.getPackageName());
+ }
+ Integer policy = mDevicePolicyEngine.getResolvedPolicy(
+ PolicyDefinition.CONTENT_PROTECTION, userHandle);
+ if (policy == null) {
+ return CONTENT_PROTECTION_DISABLED;
+ } else {
+ return policy;
+ }
+ }
+
@Override
public ManagedSubscriptionsPolicy getManagedSubscriptionsPolicy() {
synchronized (getLockObject()) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index 0fc8c5e..27f1834 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -341,6 +341,13 @@
PolicyEnforcerCallbacks.setUsbDataSignalingEnabled(value, context),
new BooleanPolicySerializer());
+ static PolicyDefinition<Integer> CONTENT_PROTECTION = new PolicyDefinition<>(
+ new NoArgsPolicyKey(DevicePolicyIdentifiers.CONTENT_PROTECTION_POLICY),
+ new MostRecent<>(),
+ POLICY_FLAG_LOCAL_ONLY_POLICY,
+ (Integer value, Context context, Integer userId, PolicyKey policyKey) -> true,
+ new IntegerPolicySerializer());
+
private static final Map<String, PolicyDefinition<?>> POLICY_DEFINITIONS = new HashMap<>();
private static Map<String, Integer> USER_RESTRICTION_FLAGS = new HashMap<>();
@@ -374,6 +381,8 @@
PERSONAL_APPS_SUSPENDED);
POLICY_DEFINITIONS.put(DevicePolicyIdentifiers.USB_DATA_SIGNALING_POLICY,
USB_DATA_SIGNALING);
+ POLICY_DEFINITIONS.put(DevicePolicyIdentifiers.CONTENT_PROTECTION_POLICY,
+ CONTENT_PROTECTION);
// User Restriction Policies
USER_RESTRICTION_FLAGS.put(UserManager.DISALLOW_MODIFY_ACCOUNTS, /* flags= */ 0);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index b79d20a..c55d709 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -48,6 +48,7 @@
import android.content.res.Configuration;
import android.content.res.Resources.Theme;
import android.credentials.CredentialManager;
+import android.credentials.flags.Flags;
import android.database.sqlite.SQLiteCompatibilityWalFlags;
import android.database.sqlite.SQLiteGlobal;
import android.graphics.GraphicsStatsService;
@@ -2795,9 +2796,14 @@
DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CREDENTIAL,
CredentialManager.DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER, true);
if (credentialManagerEnabled) {
- t.traceBegin("StartCredentialManagerService");
- mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS);
- t.traceEnd();
+ if(isWatch &&
+ !android.credentials.flags.Flags.wearCredentialManagerEnabled()) {
+ Slog.d(TAG, "CredentialManager disabled on wear.");
+ } else {
+ t.traceBegin("StartCredentialManagerService");
+ mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS);
+ t.traceEnd();
+ }
} else {
Slog.d(TAG, "CredentialManager disabled.");
}
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
new file mode 100644
index 0000000..4b578af
--- /dev/null
+++ b/services/java/com/android/server/flags.aconfig
@@ -0,0 +1,9 @@
+package: "android.server"
+
+flag {
+ namespace: "system_performance"
+ name: "telemetry_apis_service"
+ description: "Control service portion of telemetry APIs feature."
+ is_fixed_read_only: true
+ bug: "324153471"
+}
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
index d479e52..682ed91 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
+++ b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp
@@ -32,6 +32,7 @@
":BackgroundInstallControlServiceTestApp",
":BackgroundInstallControlMockApp1",
":BackgroundInstallControlMockApp2",
+ ":BackgroundInstallControlMockApp3",
],
test_suites: [
"general-tests",
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml b/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml
index 1e7a78a..a352851 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml
+++ b/services/tests/BackgroundInstallControlServiceTests/host/AndroidTest.xml
@@ -34,6 +34,9 @@
<option name="push-file"
key="BackgroundInstallControlMockApp2.apk"
value="/data/local/tmp/BackgroundInstallControlMockApp2.apk" />
+ <option name="push-file"
+ key="BackgroundInstallControlMockApp3.apk"
+ value="/data/local/tmp/BackgroundInstallControlMockApp3.apk" />
</target_preparer>
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
index c99e712..5092a46 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
+++ b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java
@@ -68,6 +68,20 @@
assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_2)).isNull();
}
+ @Test
+ public void testRegisterCallback() throws Exception {
+ runDeviceTest(
+ "BackgroundInstallControlServiceTest",
+ "testRegisterBackgroundInstallControlCallback");
+ }
+
+ @Test
+ public void testUnregisterCallback() throws Exception {
+ runDeviceTest(
+ "BackgroundInstallControlServiceTest",
+ "testUnregisterBackgroundInstallControlCallback");
+ }
+
private void installPackage(String path) throws DeviceNotAvailableException {
String cmd = "pm install -t --force-queryable " + path;
CommandResult result = getDevice().executeShellV2Command(cmd);
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
index b23f591..ac041f4 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
+++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java
@@ -16,38 +16,59 @@
package com.android.server.pm.test.app;
-import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES;
-
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.google.common.truth.Truth.assertThat;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.IBackgroundInstallControlService;
import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
+import android.os.Bundle;
+import android.os.IRemoteCallback;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.util.Pair;
+import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.ShellIdentityUtils;
+import com.android.compatibility.common.util.ThrowingRunnable;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.FileInputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
@RunWith(AndroidJUnit4.class)
public class BackgroundInstallControlServiceTest {
private static final String TAG = "BackgroundInstallControlServiceTest";
+ private static final String ACTION_INSTALL_COMMIT =
+ "com.android.server.pm.test.app.BackgroundInstallControlServiceTest"
+ + ".ACTION_INSTALL_COMMIT";
private static final String MOCK_PACKAGE_NAME = "com.android.servicestests.apps.bicmockapp3";
+ private static final String TEST_DATA_DIR = "/data/local/tmp/";
+
+ private static final String MOCK_APK_FILE = "BackgroundInstallControlMockApp3.apk";
private IBackgroundInstallControlService mIBics;
@Before
@@ -74,10 +95,9 @@
PackageManager.MATCH_ALL, Process.myUserHandle()
.getIdentifier());
} catch (RemoteException e) {
- throw new RuntimeException(e);
+ throw e.rethrowFromSystemServer();
}
- },
- GET_BACKGROUND_INSTALLED_PACKAGES);
+ });
assertThat(slice).isNotNull();
var packageList = slice.getList();
@@ -94,4 +114,150 @@
.collect(Collectors.toSet());
assertThat(actualPackageNames).containsExactlyElementsIn(expectedPackageNames);
}
+
+ @Test
+ public void testRegisterBackgroundInstallControlCallback()
+ throws Exception {
+ String testPackageName = "test";
+ int testUserId = 1;
+ ArrayList<Pair<String, Integer>> sharedResource = new ArrayList<>();
+ IRemoteCallback testCallback =
+ new IRemoteCallback.Stub() {
+ private final ArrayList<Pair<String, Integer>> mArray = sharedResource;
+
+ @Override
+ public void sendResult(Bundle data) throws RemoteException {
+ mArray.add(new Pair(testPackageName, testUserId));
+ }
+ };
+ ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
+ mIBics,
+ (bics) -> {
+ try {
+ bics.registerBackgroundInstallCallback(testCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ });
+ installPackage(TEST_DATA_DIR + MOCK_APK_FILE, MOCK_PACKAGE_NAME);
+
+ assertUntil(() -> sharedResource.size() == 1, 2000);
+ assertThat(sharedResource.get(0).first).isEqualTo(testPackageName);
+ assertThat(sharedResource.get(0).second).isEqualTo(testUserId);
+ }
+
+ @Test
+ public void testUnregisterBackgroundInstallControlCallback() {
+ String testValue = "test";
+ ArrayList<String> sharedResource = new ArrayList<>();
+ IRemoteCallback testCallback =
+ new IRemoteCallback.Stub() {
+ private final ArrayList<String> mArray = sharedResource;
+
+ @Override
+ public void sendResult(Bundle data) throws RemoteException {
+ mArray.add(testValue);
+ }
+ };
+ ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(
+ mIBics,
+ (bics) -> {
+ try {
+ bics.registerBackgroundInstallCallback(testCallback);
+ bics.unregisterBackgroundInstallCallback(testCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ });
+ installPackage(TEST_DATA_DIR + MOCK_APK_FILE, MOCK_PACKAGE_NAME);
+
+ assertUntil(sharedResource::isEmpty, 2000);
+ }
+
+ private static boolean installPackage(String apkPath, String packageName) {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ final CountDownLatch installLatch = new CountDownLatch(1);
+ final BroadcastReceiver installReceiver =
+ new BroadcastReceiver() {
+ public void onReceive(Context context, Intent intent) {
+ int packageInstallStatus =
+ intent.getIntExtra(
+ PackageInstaller.EXTRA_STATUS,
+ PackageInstaller.STATUS_FAILURE_INVALID);
+ if (packageInstallStatus == PackageInstaller.STATUS_SUCCESS) {
+ installLatch.countDown();
+ }
+ }
+ };
+ final IntentFilter intentFilter = new IntentFilter(ACTION_INSTALL_COMMIT);
+ context.registerReceiver(installReceiver, intentFilter, Context.RECEIVER_EXPORTED);
+
+ PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
+ PackageInstaller.SessionParams params =
+ new PackageInstaller.SessionParams(
+ PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+ params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED);
+ try {
+ int sessionId = packageInstaller.createSession(params);
+ PackageInstaller.Session session = packageInstaller.openSession(sessionId);
+ OutputStream out = session.openWrite(packageName, 0, -1);
+ FileInputStream fis = new FileInputStream(apkPath);
+ byte[] buffer = new byte[65536];
+ int size;
+ while ((size = fis.read(buffer)) != -1) {
+ out.write(buffer, 0, size);
+ }
+ session.fsync(out);
+ fis.close();
+ out.close();
+
+ runWithShellPermissionIdentity(
+ () -> {
+ session.commit(createPendingIntent(context).getIntentSender());
+ installLatch.await(5, TimeUnit.SECONDS);
+ });
+ return true;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static PendingIntent createPendingIntent(Context context) {
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ 1,
+ new Intent(ACTION_INSTALL_COMMIT)
+ .setPackage(
+ BackgroundInstallControlServiceTest.class.getPackageName()),
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE);
+ return pendingIntent;
+ }
+
+ private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable command)
+ throws Exception {
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+ try {
+ command.run();
+ } finally {
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+ }
+
+ private static void assertUntil(Supplier<Boolean> condition, int timeoutMs) {
+ long endTime = System.currentTimeMillis() + timeoutMs;
+ while (System.currentTimeMillis() <= endTime) {
+ if (condition.get()) return;
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ assertThat(condition.get()).isTrue();
+ }
}
\ No newline at end of file
diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp
index 7804f4c..39b0ff7 100644
--- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp
+++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/MockApp/Android.bp
@@ -50,3 +50,11 @@
"--rename-manifest-package com.android.servicestests.apps.bicmockapp2",
],
}
+
+android_test_helper_app {
+ name: "BackgroundInstallControlMockApp3",
+ defaults: ["bic-mock-app-defaults"],
+ aaptflags: [
+ "--rename-manifest-package com.android.servicestests.apps.bicmockapp3",
+ ],
+}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
index d307608..b374af6 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
@@ -149,8 +149,8 @@
callingUidInt.set(it.callingUid)
callingUserIdInt.set(it.callingUserId)
service.proxy = it.proxy
- service.addPackage(visiblePkgState)
- service.addPackage(invisiblePkgState)
+ service.addPackage(visiblePkgState, null)
+ service.addPackage(invisiblePkgState, null)
service.block(it)
}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
index 5edf30a3..a8100af 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
@@ -566,7 +566,7 @@
}
private fun DomainVerificationService.addPackages(vararg pkgStates: PackageStateInternal) =
- pkgStates.forEach(::addPackage)
+ pkgStates.forEach {pkg: PackageStateInternal -> addPackage(pkg, null)}
private fun makeManager(service: DomainVerificationService, userId: Int) =
DomainVerificationManager(
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt
index 85f0125..e0407c1 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt
@@ -21,6 +21,7 @@
import android.content.pm.Signature
import android.content.pm.SigningDetails
import android.content.pm.verify.domain.DomainOwner
+import android.content.pm.verify.domain.DomainSet
import android.content.pm.verify.domain.DomainVerificationInfo.STATE_MODIFIABLE_VERIFIED
import android.content.pm.verify.domain.DomainVerificationInfo.STATE_NO_RESPONSE
import android.content.pm.verify.domain.DomainVerificationInfo.STATE_SUCCESS
@@ -48,16 +49,16 @@
import com.android.server.testutils.spy
import com.android.server.testutils.whenever
import com.google.common.truth.Truth.assertThat
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-import java.security.PublicKey
-import java.util.UUID
import org.junit.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mockito.doReturn
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.security.PublicKey
+import java.util.UUID
class DomainVerificationPackageTest {
@@ -88,7 +89,7 @@
@Test
fun addPackageFirstTime() {
val service = makeService(pkg1, pkg2)
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
val info = service.getInfo(pkg1.packageName)
assertThat(info.packageName).isEqualTo(pkg1.packageName)
assertThat(info.identifier).isEqualTo(pkg1.domainSetId)
@@ -120,8 +121,8 @@
systemConfiguredPackageNames = ArraySet(setOf(pkg1.packageName, pkg2.packageName)),
pkg1, pkg2
)
- service.addPackage(pkg1)
- service.addPackage(pkg2)
+ service.addPackage(pkg1, null)
+ service.addPackage(pkg2, null)
service.getInfo(pkg1.packageName).apply {
assertThat(packageName).isEqualTo(pkg1.packageName)
@@ -198,7 +199,7 @@
val service = makeService(pkg1, pkg2)
val computer = mockComputer(pkg1, pkg2)
service.restoreSettings(computer, Xml.resolvePullParser(xml.byteInputStream()))
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
val info = service.getInfo(pkg1.packageName)
assertThat(info.packageName).isEqualTo(pkg1.packageName)
assertThat(info.identifier).isEqualTo(pkg1.domainSetId)
@@ -248,7 +249,7 @@
val service = makeService(pkg1, pkg2)
val computer = mockComputer(pkg1, pkg2)
service.restoreSettings(computer, Xml.resolvePullParser(xml.byteInputStream()))
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
val info = service.getInfo(pkg1.packageName)
assertThat(info.packageName).isEqualTo(pkg1.packageName)
assertThat(info.identifier).isEqualTo(pkg1.domainSetId)
@@ -307,7 +308,7 @@
service.readSettings(computer, Xml.resolvePullParser(it))
}
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
assertAddPackageActivePendingRestoredState(service)
}
@@ -321,7 +322,7 @@
service.readSettings(computer, Xml.resolvePullParser(it))
}
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
val userState = service.getUserState(pkg1.packageName)
assertThat(userState.packageName).isEqualTo(pkg1.packageName)
@@ -345,11 +346,55 @@
service.restoreSettings(computer, Xml.resolvePullParser(it))
}
- service.addPackage(pkg1)
+ service.addPackage(pkg1, null)
assertAddPackageActivePendingRestoredState(service, expectRestore = true)
}
+ @Test
+ fun addPackageWithPreVerifiedDomains() {
+ val service = makeService(pkg1)
+ val pkg1 = mockPkgState(
+ PKG_ONE,
+ UUID_ONE,
+ SIGNATURE_ONE,
+ autoVerifyDomains = listOf(DOMAIN_1, DOMAIN_2),
+ otherDomains = listOf(DOMAIN_3, DOMAIN_4))
+ service.addPackage(pkg1, DomainSet(setOf(DOMAIN_1, DOMAIN_3)))
+ val info = service.getInfo(pkg1.packageName)
+ assertThat(info.packageName).isEqualTo(pkg1.packageName)
+ assertThat(info.identifier).isEqualTo(pkg1.domainSetId)
+ // Test that DOMAIN_1 is pre-verified and DOMAIN_3 is ignored because autoVerify=false
+ assertThat(info.hostToStateMap).containsExactlyEntriesIn(mapOf(
+ DOMAIN_1 to STATE_MODIFIABLE_VERIFIED,
+ DOMAIN_2 to STATE_NO_RESPONSE,
+ ))
+
+ val userState = service.getUserState(pkg1.packageName)
+ assertThat(userState.packageName).isEqualTo(pkg1.packageName)
+ assertThat(userState.identifier).isEqualTo(pkg1.domainSetId)
+ assertThat(userState.isLinkHandlingAllowed).isEqualTo(true)
+ assertThat(userState.user.identifier).isEqualTo(USER_ID)
+ assertThat(userState.hostToStateMap).containsExactlyEntriesIn(mapOf(
+ DOMAIN_1 to DOMAIN_STATE_VERIFIED,
+ DOMAIN_2 to DOMAIN_STATE_NONE,
+ ))
+
+ assertThat(service.queryValidVerificationPackageNames())
+ .containsExactly(pkg1.packageName)
+
+ // Test that the pre-verified state can be overwritten to be disapproved
+ service.setDomainVerificationStatusInternal(
+ PKG_ONE,
+ DomainVerificationState.STATE_DENIED,
+ ArraySet(setOf(DOMAIN_1, DOMAIN_2)))
+ val infoUpdated = service.getInfo(pkg1.packageName)
+ assertThat(infoUpdated.hostToStateMap).containsExactlyEntriesIn(mapOf(
+ DOMAIN_1 to STATE_UNMODIFIABLE,
+ DOMAIN_2 to STATE_UNMODIFIABLE,
+ ))
+ }
+
/**
* Shared string that contains invalid [DOMAIN_3] and [DOMAIN_4] which should be stripped from
* the final state.
@@ -447,7 +492,7 @@
val map = mutableMapOf<String, PackageStateInternal>()
val service = makeService { map[it] }
- service.addPackage(pkgBefore)
+ service.addPackage(pkgBefore, null)
// Only insert the package after addPackage call to ensure the service doesn't access
// a live package inside the addPackage logic. It should only use the provided input.
@@ -482,7 +527,7 @@
map[pkgName] = pkgAfter
- service.migrateState(pkgBefore, pkgAfter)
+ service.migrateState(pkgBefore, pkgAfter, null)
assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf(
DOMAIN_1 to STATE_UNMODIFIABLE,
@@ -503,7 +548,7 @@
val map = mutableMapOf<String, PackageStateInternal>()
val service = makeService { map[it] }
- service.addPackage(pkgBefore)
+ service.addPackage(pkgBefore, null)
// Only insert the package after addPackage call to ensure the service doesn't access
// a live package inside the addPackage logic. It should only use the provided input.
@@ -522,7 +567,7 @@
// Now remove the package because migrateState shouldn't use it either
map.remove(pkgName)
- service.migrateState(pkgBefore, pkgAfter)
+ service.migrateState(pkgBefore, pkgAfter, null)
map[pkgName] = pkgAfter
@@ -550,7 +595,7 @@
val map = mutableMapOf<String, PackageStateInternal>()
val service = makeService { map[it] }
- service.addPackage(pkgBefore)
+ service.addPackage(pkgBefore, null)
// Only insert the package after addPackage call to ensure the service doesn't access
// a live package inside the addPackage logic. It should only use the provided input.
@@ -571,7 +616,7 @@
// Now remove the package because migrateState shouldn't use it either
map.remove(pkgName)
- service.migrateState(pkgBefore, pkgAfter)
+ service.migrateState(pkgBefore, pkgAfter, null)
map[pkgName] = pkgAfter
@@ -596,7 +641,7 @@
val map = mutableMapOf<String, PackageStateInternal>()
val service = makeService { map[it] }
- service.addPackage(pkgBefore)
+ service.addPackage(pkgBefore, null)
// Only insert the package after addPackage call to ensure the service doesn't access
// a live package inside the addPackage logic. It should only use the provided input.
@@ -615,7 +660,7 @@
// Now remove the package because migrateState shouldn't use it either
map.remove(pkgName)
- service.migrateState(pkgBefore, pkgAfter)
+ service.migrateState(pkgBefore, pkgAfter, null)
map[pkgName] = pkgAfter
@@ -640,7 +685,7 @@
val map = mutableMapOf<String, PackageStateInternal>()
val service = makeService { map[it] }
- service.addPackage(pkgBefore)
+ service.addPackage(pkgBefore, null)
// Only insert the package after addPackage call to ensure the service doesn't access
// a live package inside the addPackage logic. It should only use the provided input.
@@ -667,7 +712,7 @@
// Now remove the package because migrateState shouldn't use it either
map.remove(pkgName)
- service.migrateState(pkgBefore, pkgAfter)
+ service.migrateState(pkgBefore, pkgAfter, null)
map[pkgName] = pkgAfter
@@ -685,6 +730,30 @@
}
@Test
+ fun migratePackageWithPreVerifiedDomains() {
+ val pkgName = PKG_ONE
+ val pkgBefore = mockPkgState(pkgName, UUID_ONE, SIGNATURE_ONE, emptyList())
+ val pkgAfter = mockPkgState(pkgName, UUID_TWO, SIGNATURE_TWO, listOf(DOMAIN_1, DOMAIN_2))
+
+ val map = mutableMapOf<String, PackageStateInternal>()
+ val service = makeService { map[it] }
+ service.addPackage(pkgBefore, null)
+ service.migrateState(pkgBefore, pkgAfter, DomainSet(setOf(DOMAIN_1, DOMAIN_3)))
+
+ map[pkgName] = pkgAfter
+
+ assertThat(service.getInfo(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf(
+ DOMAIN_1 to STATE_MODIFIABLE_VERIFIED,
+ DOMAIN_2 to STATE_NO_RESPONSE,
+ ))
+ assertThat(service.getUserState(pkgName).hostToStateMap).containsExactlyEntriesIn(mapOf(
+ DOMAIN_1 to DOMAIN_STATE_VERIFIED,
+ DOMAIN_2 to DOMAIN_STATE_NONE,
+ ))
+ assertThat(service.queryValidVerificationPackageNames()).containsExactly(pkgName)
+ }
+
+ @Test
fun backupAndRestore() {
// This test acts as a proxy for true user restore through PackageManager,
// as that's much harder to test for real.
@@ -694,8 +763,8 @@
listOf(DOMAIN_1, DOMAIN_2, DOMAIN_3))
val serviceBefore = makeService(pkg1, pkg2)
val computerBefore = mockComputer(pkg1, pkg2)
- serviceBefore.addPackage(pkg1)
- serviceBefore.addPackage(pkg2)
+ serviceBefore.addPackage(pkg1, null)
+ serviceBefore.addPackage(pkg2, null)
serviceBefore.setStatus(pkg1.domainSetId, setOf(DOMAIN_1), STATE_SUCCESS)
serviceBefore.setDomainVerificationLinkHandlingAllowed(pkg1.packageName, false, 10)
@@ -748,8 +817,8 @@
val serviceAfter = makeService(pkg1, pkg2)
val computerAfter = mockComputer(pkg1, pkg2)
- serviceAfter.addPackage(pkg1)
- serviceAfter.addPackage(pkg2)
+ serviceAfter.addPackage(pkg1, null)
+ serviceAfter.addPackage(pkg2, null)
// Check the state is default before the restoration applies
listOf(0, 10).forEach {
@@ -858,8 +927,8 @@
)
val service = makeService(pkg1, pkg2)
- service.addPackage(pkg1)
- service.addPackage(pkg2)
+ service.addPackage(pkg1, null)
+ service.addPackage(pkg2, null)
// Approve domain 1, 3, and 4 for package 2 for both users
USER_IDS.forEach {
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt
index a5c4f6c..9748307 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationSettingsMutationTest.kt
@@ -106,7 +106,7 @@
fun service(name: String, block: DomainVerificationService.() -> Unit) =
Params(makeService, name) { service ->
service.proxy = proxy
- service.addPackage(mockPkgState())
+ service.addPackage(mockPkgState(), null)
service.block()
}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt
index ae570a3..56ab841 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationUserSelectionOverrideTest.kt
@@ -97,8 +97,8 @@
}
}
})
- addPackage(pkg1)
- addPackage(pkg2)
+ addPackage(pkg1, null)
+ addPackage(pkg2, null)
// Starting state for all tests is to have domain 1 enabled for the first package
setDomainVerificationUserSelection(UUID_ONE, setOf(DOMAIN_ONE), true, USER_ID)
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index 3355a6c..fab7610 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -44,6 +44,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
@@ -57,6 +58,7 @@
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.PermissionChecker;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
@@ -65,7 +67,9 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkPolicyManager;
+import android.os.BatteryManager;
import android.os.BatteryManagerInternal;
+import android.os.BatteryManagerInternal.ChargingPolicyChangeListener;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -89,6 +93,8 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
@@ -107,6 +113,8 @@
@Mock
private ActivityManagerInternal mActivityMangerInternal;
@Mock
+ private BatteryManagerInternal mBatteryManagerInternal;
+ @Mock
private Context mContext;
@Mock
private PackageManagerInternal mPackageManagerInternal;
@@ -114,6 +122,8 @@
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ private ChargingPolicyChangeListener mChargingPolicyChangeListener;
+
private class TestJobSchedulerService extends JobSchedulerService {
TestJobSchedulerService(Context context) {
super(context);
@@ -137,7 +147,7 @@
.when(() -> LocalServices.getService(ActivityManagerInternal.class));
doReturn(mock(AppStandbyInternal.class))
.when(() -> LocalServices.getService(AppStandbyInternal.class));
- doReturn(mock(BatteryManagerInternal.class))
+ doReturn(mBatteryManagerInternal)
.when(() -> LocalServices.getService(BatteryManagerInternal.class));
doReturn(mPackageManagerInternal)
.when(() -> LocalServices.getService(PackageManagerInternal.class));
@@ -186,8 +196,17 @@
// Called by DeviceIdlenessTracker
when(mContext.getSystemService(UiModeManager.class)).thenReturn(mock(UiModeManager.class));
+ setChargingPolicy(Integer.MIN_VALUE);
+
+ ArgumentCaptor<ChargingPolicyChangeListener> chargingPolicyChangeListenerCaptor =
+ ArgumentCaptor.forClass(ChargingPolicyChangeListener.class);
+
mService = new TestJobSchedulerService(mContext);
mService.waitOnAsyncLoadingForTesting();
+
+ verify(mBatteryManagerInternal).registerChargingPolicyChangeListener(
+ chargingPolicyChangeListenerCaptor.capture());
+ mChargingPolicyChangeListener = chargingPolicyChangeListenerCaptor.getValue();
}
@After
@@ -1718,6 +1737,127 @@
assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
}
+ @Test
+ public void testBatteryStateTrackerRegistersForImportantIntents() {
+ verify(mContext).registerReceiver(any(), ArgumentMatchers.argThat(filter -> true
+ && filter.hasAction(BatteryManager.ACTION_CHARGING)
+ && filter.hasAction(BatteryManager.ACTION_DISCHARGING)
+ && filter.hasAction(Intent.ACTION_BATTERY_LEVEL_CHANGED)
+ && filter.hasAction(Intent.ACTION_BATTERY_LOW)
+ && filter.hasAction(Intent.ACTION_BATTERY_OKAY)
+ && filter.hasAction(Intent.ACTION_POWER_CONNECTED)
+ && filter.hasAction(Intent.ACTION_POWER_DISCONNECTED)));
+ }
+
+ @Test
+ public void testIsCharging_standardChargingIntent() {
+ JobSchedulerService.BatteryStateTracker tracker = mService.mBatteryStateTracker;
+
+ Intent chargingIntent = new Intent(BatteryManager.ACTION_CHARGING);
+ Intent dischargingIntent = new Intent(BatteryManager.ACTION_DISCHARGING);
+ tracker.onReceive(mContext, dischargingIntent);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ tracker.onReceive(mContext, chargingIntent);
+ assertTrue(tracker.isCharging());
+ assertTrue(mService.isBatteryCharging());
+
+ tracker.onReceive(mContext, dischargingIntent);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+ }
+
+ @Test
+ public void testIsCharging_adaptiveCharging_batteryTooLow() {
+ JobSchedulerService.BatteryStateTracker tracker = mService.mBatteryStateTracker;
+
+ tracker.onReceive(mContext, new Intent(BatteryManager.ACTION_DISCHARGING));
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(15);
+ setChargingPolicy(BatteryManager.CHARGING_POLICY_ADAPTIVE_AC);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ tracker.onReceive(mContext, new Intent(Intent.ACTION_POWER_CONNECTED));
+
+ setBatteryLevel(70);
+ assertTrue(tracker.isCharging());
+ assertTrue(mService.isBatteryCharging());
+ }
+
+ @Test
+ public void testIsCharging_adaptiveCharging_chargeBelowThreshold() {
+ JobSchedulerService.BatteryStateTracker tracker = mService.mBatteryStateTracker;
+
+ setChargingPolicy(BatteryManager.CHARGING_POLICY_ADAPTIVE_AC);
+ tracker.onReceive(mContext, new Intent(Intent.ACTION_POWER_CONNECTED));
+ setBatteryLevel(5);
+
+ tracker.onReceive(mContext, new Intent(BatteryManager.ACTION_CHARGING));
+ assertTrue(tracker.isCharging());
+ assertTrue(mService.isBatteryCharging());
+
+ for (int level = 5; level < 80; ++level) {
+ setBatteryLevel(level);
+ assertTrue(tracker.isCharging());
+ assertTrue(mService.isBatteryCharging());
+ }
+ }
+
+ @Test
+ public void testIsCharging_adaptiveCharging_dischargeAboveThreshold() {
+ JobSchedulerService.BatteryStateTracker tracker = mService.mBatteryStateTracker;
+
+ setChargingPolicy(BatteryManager.CHARGING_POLICY_ADAPTIVE_AC);
+ tracker.onReceive(mContext, new Intent(Intent.ACTION_POWER_CONNECTED));
+ setBatteryLevel(80);
+
+ tracker.onReceive(mContext, new Intent(BatteryManager.ACTION_DISCHARGING));
+ assertTrue(tracker.isCharging());
+ assertTrue(mService.isBatteryCharging());
+
+ for (int level = 80; level > 60; --level) {
+ setBatteryLevel(level);
+ assertEquals(level >= 70, tracker.isCharging());
+ assertEquals(level >= 70, mService.isBatteryCharging());
+ }
+ }
+
+ @Test
+ public void testIsCharging_adaptiveCharging_notPluggedIn() {
+ JobSchedulerService.BatteryStateTracker tracker = mService.mBatteryStateTracker;
+
+ tracker.onReceive(mContext, new Intent(Intent.ACTION_POWER_DISCONNECTED));
+ tracker.onReceive(mContext, new Intent(BatteryManager.ACTION_DISCHARGING));
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(15);
+ setChargingPolicy(BatteryManager.CHARGING_POLICY_ADAPTIVE_AC);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(50);
+ setChargingPolicy(BatteryManager.CHARGING_POLICY_ADAPTIVE_AC);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(70);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(95);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+
+ setBatteryLevel(100);
+ assertFalse(tracker.isCharging());
+ assertFalse(mService.isBatteryCharging());
+ }
+
/** Tests that rare job batching works as expected. */
@Test
public void testConnectivityJobBatching() {
@@ -2257,4 +2397,17 @@
assertFalse(mService.getPendingJobQueue().contains(job2b));
assertEquals(JobScheduler.PENDING_JOB_REASON_USER, mService.getPendingJobReason(job2b));
}
+
+ private void setBatteryLevel(int level) {
+ doReturn(level).when(mBatteryManagerInternal).getBatteryLevel();
+ mService.mBatteryStateTracker
+ .onReceive(mContext, new Intent(Intent.ACTION_BATTERY_LEVEL_CHANGED));
+ }
+
+ private void setChargingPolicy(int policy) {
+ doReturn(policy).when(mBatteryManagerInternal).getChargingPolicy();
+ if (mChargingPolicyChangeListener != null) {
+ mChargingPolicyChangeListener.onChargingPolicyChanged(policy);
+ }
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
index d4ef647..ea937de 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
@@ -25,14 +25,11 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import static org.mockito.Mockito.verify;
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.os.BatteryManagerInternal;
@@ -49,8 +46,6 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
@@ -63,7 +58,6 @@
private BatteryController mBatteryController;
private FlexibilityController mFlexibilityController;
- private BroadcastReceiver mPowerReceiver;
private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants();
private int mSourceUid;
@@ -99,10 +93,6 @@
.when(() -> LocalServices.getService(PackageManagerInternal.class));
// Initialize real objects.
- // Capture the listeners.
- ArgumentCaptor<BroadcastReceiver> receiverCaptor =
- ArgumentCaptor.forClass(BroadcastReceiver.class);
-
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mPackageManager.hasSystemFeature(
PackageManager.FEATURE_AUTOMOTIVE)).thenReturn(false);
@@ -111,11 +101,6 @@
mBatteryController = new BatteryController(mJobSchedulerService, mFlexibilityController);
mBatteryController.startTrackingLocked();
- verify(mContext).registerReceiver(receiverCaptor.capture(),
- ArgumentMatchers.argThat(filter ->
- filter.hasAction(Intent.ACTION_POWER_CONNECTED)
- && filter.hasAction(Intent.ACTION_POWER_DISCONNECTED)));
- mPowerReceiver = receiverCaptor.getValue();
try {
mSourceUid = AppGlobals.getPackageManager().getPackageUid(SOURCE_PACKAGE, 0, 0);
// Need to do this since we're using a mock JS and not a real object.
@@ -159,9 +144,11 @@
}
private void setPowerConnected(boolean connected) {
- Intent intent = new Intent(
- connected ? Intent.ACTION_POWER_CONNECTED : Intent.ACTION_POWER_DISCONNECTED);
- mPowerReceiver.onReceive(mContext, intent);
+ doReturn(connected).when(mJobSchedulerService).isPowerConnected();
+ synchronized (mBatteryController.mLock) {
+ mBatteryController.onBatteryStateChangedLocked();
+ }
+ waitForNonDelayedMessagesProcessed();
}
private void setUidBias(int uid, int bias) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java
new file mode 100644
index 0000000..574f369
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.server.pm;
+
+import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_PACKAGE_NAME_KEY;
+import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_USER_ID_KEY;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.os.Bundle;
+import android.os.IRemoteCallback;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+
+/** Unit tests for {@link BackgroundInstallControlCallbackHelper} */
+@Presubmit
+@RunWith(JUnit4.class)
+public class BackgroundInstallControlCallbackHelperTest {
+
+ private final IRemoteCallback mCallback =
+ spy(
+ new IRemoteCallback.Stub() {
+ @Override
+ public void sendResult(Bundle extras) {}
+ });
+
+ private BackgroundInstallControlCallbackHelper mCallbackHelper;
+
+ @Before
+ public void setup() {
+ mCallbackHelper = new BackgroundInstallControlCallbackHelper();
+ }
+
+ @Test
+ public void registerBackgroundInstallControlCallback_registers_successfully() {
+ mCallbackHelper.registerBackgroundInstallCallback(mCallback);
+
+ synchronized (mCallbackHelper.mCallbacks) {
+ assertEquals(1, mCallbackHelper.mCallbacks.getRegisteredCallbackCount());
+ assertEquals(mCallback, mCallbackHelper.mCallbacks.getRegisteredCallbackItem(0));
+ }
+ }
+
+ @Test
+ public void unregisterBackgroundInstallControlCallback_unregisters_successfully() {
+ synchronized (mCallbackHelper.mCallbacks) {
+ mCallbackHelper.mCallbacks.register(mCallback);
+ }
+
+ mCallbackHelper.unregisterBackgroundInstallCallback(mCallback);
+
+ synchronized (mCallbackHelper.mCallbacks) {
+ assertEquals(0, mCallbackHelper.mCallbacks.getRegisteredCallbackCount());
+ }
+ }
+
+ @Test
+ public void notifyAllCallbacks_broadcastsToCallbacks()
+ throws RemoteException {
+ String testPackageName = "testname";
+ int testUserId = 1;
+ mCallbackHelper.registerBackgroundInstallCallback(mCallback);
+
+ mCallbackHelper.notifyAllCallbacks(testUserId, testPackageName);
+
+ ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+ verify(mCallback, after(1000).times(1)).sendResult(bundleCaptor.capture());
+ Bundle receivedBundle = bundleCaptor.getValue();
+ assertEquals(testPackageName, receivedBundle.getString(FLAGGED_PACKAGE_NAME_KEY));
+ assertEquals(testUserId, receivedBundle.getInt(FLAGGED_USER_ID_KEY));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java
index 2dfabd0..b12d6da 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java
@@ -893,13 +893,13 @@
@Test
public void getTokenLocked_windowIsRegistered_shouldReturnToken() {
- final IBinder token = mA11yWindowManager.getTokenLocked(HOST_WINDOW_ID);
+ final IBinder token = mA11yWindowManager.getLeashTokenLocked(HOST_WINDOW_ID);
assertEquals(token, mMockHostToken);
}
@Test
public void getTokenLocked_windowIsNotRegistered_shouldReturnNull() {
- final IBinder token = mA11yWindowManager.getTokenLocked(OTHER_WINDOW_ID);
+ final IBinder token = mA11yWindowManager.getLeashTokenLocked(OTHER_WINDOW_ID);
assertNull(token);
}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index 5943832..07e6ab2 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -19,11 +19,10 @@
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
-import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import android.hardware.display.DisplayManagerInternal;
@@ -86,9 +85,8 @@
LocalServices.removeServiceForTest(InputManagerInternal.class);
LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock);
- final DisplayInfo displayInfo = new DisplayInfo();
- displayInfo.uniqueId = "uniqueId";
- doReturn(displayInfo).when(mDisplayManagerInternalMock).getDisplayInfo(anyInt());
+ setUpDisplay(1 /* displayId */);
+ setUpDisplay(2 /* displayId */);
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
@@ -100,6 +98,16 @@
threadVerifier);
}
+ void setUpDisplay(int displayId) {
+ final String uniqueId = "uniqueId:" + displayId;
+ doAnswer((inv) -> {
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.uniqueId = uniqueId;
+ return displayInfo;
+ }).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId));
+ mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId);
+ }
+
@After
public void tearDown() {
mInputManagerMockHelper.tearDown();
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
index 3722247..74e854e4 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
@@ -20,7 +20,6 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import android.hardware.input.IInputDevicesChangedListener;
@@ -28,12 +27,15 @@
import android.hardware.input.InputManagerGlobal;
import android.os.RemoteException;
import android.testing.TestableLooper;
+import android.view.Display;
import android.view.InputDevice;
import org.mockito.invocation.InvocationOnMock;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.stream.IntStream;
@@ -49,6 +51,10 @@
private final InputManagerGlobal.TestSession mInputManagerGlobalSession;
private final List<InputDevice> mDevices = new ArrayList<>();
private IInputDevicesChangedListener mDevicesChangedListener;
+ private final Map<String /* uniqueId */, Integer /* displayId */> mDisplayIdMapping =
+ new HashMap<>();
+ private final Map<String /* phys */, String /* uniqueId */> mUniqueIdAssociation =
+ new HashMap<>();
InputManagerMockHelper(TestableLooper testableLooper,
InputController.NativeWrapper nativeWrapperMock, IInputManager iInputManagerMock)
@@ -73,8 +79,10 @@
when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]);
doAnswer(inv -> mDevices.get(inv.getArgument(0)))
.when(mIInputManagerMock).getInputDevice(anyInt());
- doNothing().when(mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString());
- doNothing().when(mIInputManagerMock).removeUniqueIdAssociation(anyString());
+ doAnswer(inv -> mUniqueIdAssociation.put(inv.getArgument(0), inv.getArgument(1))).when(
+ mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString());
+ doAnswer(inv -> mUniqueIdAssociation.remove(inv.getArgument(0))).when(
+ mIInputManagerMock).removeUniqueIdAssociation(anyString());
// Set a new instance of InputManager for testing that uses the IInputManager mock as the
// interface to the server.
@@ -87,17 +95,25 @@
}
}
+ public void addDisplayIdMapping(String uniqueId, int displayId) {
+ mDisplayIdMapping.put(uniqueId, displayId);
+ }
+
private long handleNativeOpenInputDevice(InvocationOnMock inv) {
Objects.requireNonNull(mDevicesChangedListener,
"InputController did not register an InputDevicesChangedListener.");
+ final String phys = inv.getArgument(3);
final InputDevice device = new InputDevice.Builder()
.setId(mDevices.size())
.setName(inv.getArgument(0))
.setVendorId(inv.getArgument(1))
.setProductId(inv.getArgument(2))
- .setDescriptor(inv.getArgument(3))
+ .setDescriptor(phys)
.setExternal(true)
+ .setAssociatedDisplayId(
+ mDisplayIdMapping.getOrDefault(mUniqueIdAssociation.get(phys),
+ Display.INVALID_DISPLAY))
.build();
mDevices.add(device);
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 5442af8..157e893 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
@@ -39,6 +39,7 @@
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -2015,6 +2016,13 @@
eq(virtualDevice), any(), any())).thenReturn(displayId);
virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback,
NONBLOCKED_APP_PACKAGE_NAME);
+ final String uniqueId = UNIQUE_ID + displayId;
+ doAnswer(inv -> {
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.uniqueId = uniqueId;
+ return displayInfo;
+ }).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId));
+ mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId);
}
private ComponentName getPermissionDialogComponent() {
diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
index 8656f60..bf87e3a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java
@@ -111,6 +111,8 @@
private UsageStatsManagerInternal mUsageStatsManagerInternal;
@Mock
private PermissionManagerServiceInternal mPermissionManager;
+ @Mock
+ private BackgroundInstallControlCallbackHelper mCallbackHelper;
@Captor
private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor;
@@ -982,5 +984,11 @@
public File getDiskFile() {
return mFile;
}
+
+
+ @Override
+ public BackgroundInstallControlCallbackHelper getBackgroundInstallControlCallbackHelper() {
+ return mCallbackHelper;
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 3778a32..f1d3ba9 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -23,7 +23,6 @@
import android.content.pm.UserProperties;
import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.util.Xml;
import androidx.test.filters.MediumTest;
@@ -32,7 +31,6 @@
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -54,13 +52,10 @@
@RunWith(AndroidJUnit4.class)
@MediumTest
public class UserManagerServiceUserPropertiesTest {
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
/** Test that UserProperties can properly read the xml information that it writes. */
@Test
public void testWriteReadXml() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
final UserProperties defaultProps = new UserProperties.Builder()
.setShowInLauncher(21)
.setStartWithParent(false)
@@ -123,7 +118,6 @@
/** Tests parcelling an object in which all properties are present. */
@Test
public void testParcelUnparcel() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
final UserProperties originalProps = new UserProperties.Builder()
.setShowInLauncher(2145)
.build();
@@ -134,7 +128,6 @@
/** Tests copying a UserProperties object varying permissions. */
@Test
public void testCopyLacksPermissions() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
final UserProperties defaultProps = new UserProperties.Builder()
.setShowInLauncher(2145)
.setStartWithParent(true)
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 6cdbc74..3047bcf 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -41,7 +41,6 @@
import android.os.Bundle;
import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArrayMap;
import androidx.test.InstrumentationRegistry;
@@ -51,7 +50,6 @@
import com.android.frameworks.servicestests.R;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -73,11 +71,8 @@
public void setup() {
mResources = InstrumentationRegistry.getTargetContext().getResources();
}
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Test
public void testUserTypeBuilder_createUserType() {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
final Bundle restrictions = makeRestrictionsBundle("r1", "r2");
final Bundle systemSettings = makeSettingsBundle("s1", "s2");
final Bundle secureSettings = makeSettingsBundle("secure_s1", "secure_s2");
@@ -207,7 +202,6 @@
@Test
public void testUserTypeBuilder_defaults() {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
UserTypeDetails type = new UserTypeDetails.Builder()
.setName("name") // Required (no default allowed)
.setBaseType(FLAG_FULL) // Required (no default allowed)
@@ -321,7 +315,6 @@
/** Tests {@link UserTypeFactory#customizeBuilders} for a reasonable xml file. */
@Test
public void testUserTypeFactoryCustomize_profile() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
final String userTypeAosp1 = "android.test.1"; // Profile user that is not customized
final String userTypeAosp2 = "android.test.2"; // Profile user that is customized
final String userTypeOem1 = "custom.test.1"; // Custom-defined profile
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 9323b48..df2069e 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -39,7 +39,6 @@
import android.os.UserManager;
import android.platform.test.annotations.Postsubmit;
import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.MediumTest;
@@ -56,7 +55,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -99,8 +97,6 @@
private UserSwitchWaiter mUserSwitchWaiter;
private UserRemovalWaiter mUserRemovalWaiter;
private int mOriginalCurrentUserId;
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setUp() throws Exception {
@@ -172,7 +168,6 @@
@Test
public void testCloneUser() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
assumeCloneEnabled();
UserHandle mainUser = mUserManager.getMainUser();
assumeTrue("Main user is null", mainUser != null);
@@ -229,7 +224,8 @@
.isEqualTo(cloneUserProperties.getCrossProfileContentSharingStrategy());
assertThrows(SecurityException.class, cloneUserProperties::getDeleteAppWithParent);
assertThrows(SecurityException.class, cloneUserProperties::getAlwaysVisible);
- assertThrows(SecurityException.class, cloneUserProperties::getProfileApiVisibility);
+ assertThat(typeProps.getProfileApiVisibility()).isEqualTo(
+ cloneUserProperties.getProfileApiVisibility());
compareDrawables(mUserManager.getUserBadge(),
Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain()));
@@ -311,7 +307,6 @@
@Test
public void testPrivateProfile() throws Exception {
- mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_HIDING_PROFILES);
UserHandle mainUser = mUserManager.getMainUser();
assumeTrue("Main user is null", mainUser != null);
// Get the default properties for private profile user type.
@@ -353,8 +348,8 @@
assertThrows(SecurityException.class, privateProfileUserProperties::getDeleteAppWithParent);
assertThrows(SecurityException.class,
privateProfileUserProperties::getAllowStoppingUserWithDelayedLocking);
- assertThrows(SecurityException.class,
- privateProfileUserProperties::getProfileApiVisibility);
+ assertThat(typeProps.getProfileApiVisibility()).isEqualTo(
+ privateProfileUserProperties.getProfileApiVisibility());
assertThrows(SecurityException.class,
privateProfileUserProperties::areItemsRestrictedOnHomeScreen);
compareDrawables(mUserManager.getUserBadge(),
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 f6cf4da..77be01c 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -323,6 +323,7 @@
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -12971,6 +12972,35 @@
}
@Test
+ public void fixNotification_customAllowlistToken()
+ throws Exception {
+ Notification n = new Notification.Builder(mContext, "test")
+ .build();
+ try {
+ Field allowlistToken = Class.forName("android.app.Notification").
+ getDeclaredField("mAllowlistToken");
+ allowlistToken.setAccessible(true);
+ allowlistToken.set(n, new Binder());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ mService.fixNotification(n, PKG, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE, true);
+
+ IBinder actual = null;
+ try {
+ Field allowlistToken = Class.forName("android.app.Notification").
+ getDeclaredField("mAllowlistToken");
+ allowlistToken.setAccessible(true);
+ actual = (IBinder) allowlistToken.get(n);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ assertTrue(mService.ALLOWLIST_TOKEN == actual);
+ }
+
+ @Test
public void testCancelAllNotifications_IgnoreUserInitiatedJob() throws Exception {
when(mJsi.isNotificationAssociatedWithAnyUserInitiatedJobs(anyInt(), anyInt(), anyString()))
.thenReturn(true);
diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java
index b7706a9..6bdc43e 100644
--- a/telecomm/java/android/telecom/PhoneAccount.java
+++ b/telecomm/java/android/telecom/PhoneAccount.java
@@ -810,6 +810,9 @@
* If this setter method is never called or cleared using
* {@link #clearSimultaneousCallingRestriction()}, there is no restriction and all
* {@link PhoneAccount}s registered to Telecom by this package support simultaneous calling.
+ * If this setter is called and set as an empty Set, then this {@link PhoneAccount} does
+ * not support simultaneous calling with any other {@link PhoneAccount}s registered by the
+ * same application.
* <p>
* Note: Simultaneous calling restrictions can only be placed on {@link PhoneAccount}s that
* were registered by the same application. Simultaneous calling across applications is
diff --git a/telephony/java/android/service/euicc/EuiccService.java b/telephony/java/android/service/euicc/EuiccService.java
index b59e855..5af2c34 100644
--- a/telephony/java/android/service/euicc/EuiccService.java
+++ b/telephony/java/android/service/euicc/EuiccService.java
@@ -19,6 +19,7 @@
import android.Manifest;
import android.annotation.CallSuper;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,6 +39,8 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.telephony.flags.Flags;
+
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Retention;
@@ -759,6 +762,20 @@
public abstract int onRetainSubscriptionsForFactoryReset(int slotId);
/**
+ * Return the available memory in bytes of the eUICC.
+ *
+ * @param slotId ID of the SIM slot being queried.
+ * @return the available memory in bytes.
+ * @see android.telephony.euicc.EuiccManager#getAvailableMemoryInBytes
+ */
+ @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY)
+ public long onGetAvailableMemoryInBytes(int slotId) {
+ // stub implementation, LPA needs to implement this
+ throw new UnsupportedOperationException("The connected LPA does not implement"
+ + "EuiccService#onGetAvailableMemoryInBytes(int)");
+ }
+
+ /**
* Dump to a provided printWriter.
*/
public void dump(@NonNull PrintWriter printWriter) {
@@ -834,6 +851,22 @@
}
@Override
+ @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY)
+ public void getAvailableMemoryInBytes(
+ int slotId, IGetAvailableMemoryInBytesCallback callback) {
+ mExecutor.execute(
+ () -> {
+ long availableMemoryInBytes =
+ EuiccService.this.onGetAvailableMemoryInBytes(slotId);
+ try {
+ callback.onSuccess(availableMemoryInBytes);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ });
+ }
+
+ @Override
public void startOtaIfNecessary(
int slotId, IOtaStatusChangedCallback statusChangedCallback) {
mExecutor.execute(new Runnable() {
diff --git a/telephony/java/android/service/euicc/IEuiccService.aidl b/telephony/java/android/service/euicc/IEuiccService.aidl
index f8d5ae9..0f8c72b 100644
--- a/telephony/java/android/service/euicc/IEuiccService.aidl
+++ b/telephony/java/android/service/euicc/IEuiccService.aidl
@@ -19,6 +19,7 @@
import android.service.euicc.IDeleteSubscriptionCallback;
import android.service.euicc.IDownloadSubscriptionCallback;
import android.service.euicc.IEraseSubscriptionsCallback;
+import android.service.euicc.IGetAvailableMemoryInBytesCallback;
import android.service.euicc.IGetDefaultDownloadableSubscriptionListCallback;
import android.service.euicc.IGetDownloadableSubscriptionMetadataCallback;
import android.service.euicc.IGetEidCallback;
@@ -60,4 +61,5 @@
void retainSubscriptionsForFactoryReset(
int slotId, in IRetainSubscriptionsForFactoryResetCallback callback);
void dump(in IEuiccServiceDumpResultCallback callback);
-}
\ No newline at end of file
+ void getAvailableMemoryInBytes(int slotId, in IGetAvailableMemoryInBytesCallback callback);
+}
diff --git a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java b/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl
similarity index 62%
rename from packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
rename to telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl
index 48fd4fe..bd6d19b 100644
--- a/packages/SoundPicker2/src/com/android/soundpicker/RingtonePickerApplication.java
+++ b/telephony/java/android/service/euicc/IGetAvailableMemoryInBytesCallback.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,15 +14,9 @@
* limitations under the License.
*/
-package com.android.soundpicker;
+package android.service.euicc;
-import android.app.Application;
-
-import dagger.hilt.android.HiltAndroidApp;
-
-/**
- * The main application class for the project.
- */
-@HiltAndroidApp(Application.class)
-public class RingtonePickerApplication extends Hilt_RingtonePickerApplication {
+/** @hide */
+oneway interface IGetAvailableMemoryInBytesCallback {
+ void onSuccess(long availableMemoryInBytes);
}
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index a188581..82ed340 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -40,6 +40,7 @@
import android.telephony.SubscriptionManager.ProfileClass;
import android.telephony.SubscriptionManager.SimDisplayNameSource;
import android.telephony.SubscriptionManager.SubscriptionType;
+import android.telephony.SubscriptionManager.TransferStatus;
import android.telephony.SubscriptionManager.UsageSetting;
import android.text.TextUtils;
import android.util.DisplayMetrics;
@@ -236,6 +237,11 @@
@UsageSetting
private final int mUsageSetting;
+ /**
+ * Subscription's transfer status
+ */
+ private final int mTransferStatus;
+
// Below are the fields that do not exist in the database.
/**
@@ -393,6 +399,7 @@
this.mUsageSetting = usageSetting;
this.mIsOnlyNonTerrestrialNetwork = false;
this.mServiceCapabilities = 0;
+ this.mTransferStatus = 0;
}
/**
@@ -433,6 +440,7 @@
this.mUsageSetting = builder.mUsageSetting;
this.mIsOnlyNonTerrestrialNetwork = builder.mIsOnlyNonTerrestrialNetwork;
this.mServiceCapabilities = builder.mServiceCapabilities;
+ this.mTransferStatus = builder.mTransferStatus;
}
/**
@@ -928,6 +936,19 @@
return SubscriptionManager.getServiceCapabilitiesSet(mServiceCapabilities);
}
+ /**
+ * Get the transfer status for this subscription.
+ *
+ * @return The transfer status for this subscription.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ public @TransferStatus int getTransferStatus() {
+ return mTransferStatus;
+ }
+
@NonNull
public static final Parcelable.Creator<SubscriptionInfo> CREATOR =
new Parcelable.Creator<SubscriptionInfo>() {
@@ -967,6 +988,7 @@
.setOnlyNonTerrestrialNetwork(source.readBoolean())
.setServiceCapabilities(
SubscriptionManager.getServiceCapabilitiesSet(source.readInt()))
+ .setTransferStatus(source.readInt())
.build();
}
@@ -1010,6 +1032,7 @@
dest.writeInt(mUsageSetting);
dest.writeBoolean(mIsOnlyNonTerrestrialNetwork);
dest.writeInt(mServiceCapabilities);
+ dest.writeInt(mTransferStatus);
}
@Override
@@ -1075,6 +1098,7 @@
+ " isOnlyNonTerrestrialNetwork=" + mIsOnlyNonTerrestrialNetwork
+ " serviceCapabilities=" + SubscriptionManager.getServiceCapabilitiesSet(
mServiceCapabilities).toString()
+ + " transferStatus=" + mTransferStatus
+ "]";
}
@@ -1101,7 +1125,8 @@
that.mCarrierConfigAccessRules) && Objects.equals(mGroupUuid, that.mGroupUuid)
&& mCountryIso.equals(that.mCountryIso) && mGroupOwner.equals(that.mGroupOwner)
&& mIsOnlyNonTerrestrialNetwork == that.mIsOnlyNonTerrestrialNetwork
- && mServiceCapabilities == that.mServiceCapabilities;
+ && mServiceCapabilities == that.mServiceCapabilities
+ && mTransferStatus == that.mTransferStatus;
}
@Override
@@ -1110,7 +1135,8 @@
mDisplayNameSource, mIconTint, mNumber, mDataRoaming, mMcc, mMnc, mIsEmbedded,
mCardString, mIsOpportunistic, mGroupUuid, mCountryIso, mCarrierId, mProfileClass,
mType, mGroupOwner, mAreUiccApplicationsEnabled, mPortIndex, mUsageSetting, mCardId,
- mIsGroupDisabled, mIsOnlyNonTerrestrialNetwork, mServiceCapabilities);
+ mIsGroupDisabled, mIsOnlyNonTerrestrialNetwork, mServiceCapabilities,
+ mTransferStatus);
result = 31 * result + Arrays.hashCode(mEhplmns);
result = 31 * result + Arrays.hashCode(mHplmns);
result = 31 * result + Arrays.hashCode(mNativeAccessRules);
@@ -1314,6 +1340,8 @@
*/
private boolean mIsOnlyNonTerrestrialNetwork = false;
+ private int mTransferStatus = 0;
+
/**
* Service capabilities bitmasks the subscription supports.
*/
@@ -1363,6 +1391,7 @@
mUsageSetting = info.mUsageSetting;
mIsOnlyNonTerrestrialNetwork = info.mIsOnlyNonTerrestrialNetwork;
mServiceCapabilities = info.mServiceCapabilities;
+ mTransferStatus = info.mTransferStatus;
}
/**
@@ -1785,6 +1814,18 @@
mServiceCapabilities = combinedCapabilities;
return this;
}
+ /**
+ * Set subscription's transfer status
+ *
+ * @param status Subscription's transfer status
+ * @return The builder.
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @NonNull
+ public Builder setTransferStatus(@TransferStatus int status) {
+ mTransferStatus = status;
+ return this;
+ }
/**
* Build the {@link SubscriptionInfo}.
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 1749545..3fde3b6 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1148,6 +1148,14 @@
*/
public static final String SERVICE_CAPABILITIES = SimInfo.COLUMN_SERVICE_CAPABILITIES;
+ /**
+ * TelephonyProvider column name to identify eSIM's transfer status.
+ * By default, it's disabled.
+ * <P>Type: INTEGER (int)</P>
+ * @hide
+ */
+ public static final String TRANSFER_STATUS = SimInfo.COLUMN_TRANSFER_STATUS;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"USAGE_SETTING_"},
@@ -1453,6 +1461,41 @@
private static final LruCache<Pair<String, Configuration>, Resources> sResourcesCache =
new LruCache<>(MAX_RESOURCE_CACHE_ENTRY_COUNT);
+
+ /**
+ * The profile has not been transferred or converted to an eSIM.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ public static final int TRANSFER_STATUS_NONE = 0;
+
+ /**
+ * The existing profile of the old device has been transferred to an eSIM of the new device.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ public static final int TRANSFER_STATUS_TRANSFERRED_OUT = 1;
+
+ /**
+ * The existing profile of the same device has been converted to an eSIM of the same device
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ public static final int TRANSFER_STATUS_CONVERTED = 2;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"TRANSFER_STATUS"},
+ value = {
+ TRANSFER_STATUS_NONE,
+ TRANSFER_STATUS_TRANSFERRED_OUT,
+ TRANSFER_STATUS_CONVERTED,
+ })
+ public @interface TransferStatus {}
+
+
/**
* A listener class for monitoring changes to {@link SubscriptionInfo} records.
* <p>
@@ -4716,4 +4759,27 @@
public static int serviceCapabilityToBitmask(@ServiceCapability int capability) {
return 1 << (capability - 1);
}
+
+ /**
+ * Set the transfer status of the subscriptionInfo of the subId.
+ * @param subscriptionId The unique SubscriptionInfo key in database.
+ * @param status The transfer status to change.
+ *
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
+ public void setTransferStatus(int subscriptionId, @TransferStatus int status) {
+ try {
+ ISub iSub = TelephonyManager.getSubscriptionService();
+ if (iSub != null) {
+ iSub.setTransferStatus(subscriptionId, status);
+ }
+ } catch (RemoteException ex) {
+ logd("setTransferStatus for subId = " + subscriptionId + " failed.");
+ throw ex.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index 9b8e62f..7935d24 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -16,12 +16,14 @@
package android.telephony.euicc;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresFeature;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
+import android.annotation.SuppressAutoDoc;
import android.annotation.SystemApi;
import android.app.Activity;
import android.app.PendingIntent;
@@ -50,6 +52,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -861,6 +864,10 @@
*/
public static final int ERROR_INVALID_PORT = 10017;
+ /** Temporary failure to retrieve available memory because eUICC is not ready. */
+ @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY)
+ public static final long EUICC_MEMORY_FIELD_UNAVAILABLE = -1L;
+
/**
* Apps targeting on Android T and beyond will get exception whenever switchToSubscription
* without portIndex is called for disable subscription.
@@ -961,6 +968,35 @@
}
/**
+ * Returns the available memory in bytes of the eUICC.
+ *
+ * @return the available memory in bytes. May be {@link #EUICC_MEMORY_FIELD_UNAVAILABLE} if the
+ * eUICC is not ready. Check {@link #isEnabled} for more information.
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_EUICC} or
+ * device doesn't support querying this information from the eUICC.
+ */
+ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
+ @FlaggedApi(Flags.FLAG_ESIM_AVAILABLE_MEMORY)
+ @RequiresPermission(
+ anyOf = {
+ Manifest.permission.READ_PHONE_STATE,
+ Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+ "carrier privileges"
+ })
+ public long getAvailableMemoryInBytes() {
+ if (!isEnabled()) {
+ return EUICC_MEMORY_FIELD_UNAVAILABLE;
+ }
+ try {
+ return getIEuiccController()
+ .getAvailableMemoryInBytes(mCardId, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns the current status of eUICC OTA.
*
* <p>Requires the {@link android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission.
@@ -1707,4 +1743,57 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Sets the supported carrier ids for pSIM conversion.
+ *
+ * <p>Any existing pSIM conversion supported carrier list will be replaced
+ * by the {@code carrierIds} set here.
+ *
+ * @param carrierIds is a list of carrierIds that supports pSIM conversion
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
+ * @throws IllegalStateException if this method is called when {@link #isEnabled} is false.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
+ public void setPsimConversionSupportedCarriers(@NonNull Set<Integer> carrierIds) {
+ if (!isEnabled()) {
+ throw new IllegalStateException("Euicc is not enabled");
+ }
+ try {
+ int[] arr = carrierIds.stream().mapToInt(Integer::intValue).toArray();
+ getIEuiccController().setPsimConversionSupportedCarriers(arr);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Returns whether the given carrier id supports pSIM conversion or not.
+ *
+ * @param carrierId to check whether pSIM conversion is supported or not
+ * @return whether the given carrier id supports pSIM conversion or not,
+ * or false if {@link #isEnabled} is false
+ *
+ * @throws UnsupportedOperationException If the device does not have
+ * {@link PackageManager#FEATURE_TELEPHONY_EUICC}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_SUPPORT_PSIM_TO_ESIM_CONVERSION)
+ @SystemApi
+ @RequiresPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS)
+ public boolean isPsimConversionSupported(int carrierId) {
+ if (!isEnabled()) {
+ return false;
+ }
+ try {
+ return getIEuiccController().isPsimConversionSupported(carrierId);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
}
diff --git a/telephony/java/android/telephony/satellite/EnableRequestAttributes.java b/telephony/java/android/telephony/satellite/EnableRequestAttributes.java
new file mode 100644
index 0000000..bc9d230
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/EnableRequestAttributes.java
@@ -0,0 +1,145 @@
+/*
+ * 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.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import com.android.internal.telephony.flags.Flags;
+
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * EnableRequestAttributes is used to store the attributes of the request
+ * {@link SatelliteManager#requestEnabled(EnableRequestAttributes, Executor, Consumer)}
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+public class EnableRequestAttributes {
+ /** {@code true} to enable satellite and {@code false} to disable satellite */
+ private boolean mIsEnabled;
+ /**
+ * {@code true} to enable demo mode and {@code false} to disable. When disabling satellite,
+ * {@code mIsDemoMode} is always considered as {@code false} by Telephony.
+ */
+ private boolean mIsDemoMode;
+ /**
+ * {@code true} means satellite is enabled for emergency mode, {@code false} otherwise. When
+ * disabling satellite, {@code isEmergencyMode} is always considered as {@code false} by
+ * Telephony.
+ */
+ private boolean mIsEmergencyMode;
+
+ /**
+ * Constructor from builder.
+ *
+ * @param builder Builder of {@link EnableRequestAttributes}.
+ */
+ private EnableRequestAttributes(@NonNull Builder builder) {
+ this.mIsEnabled = builder.mIsEnabled;
+ this.mIsDemoMode = builder.mIsDemoMode;
+ this.mIsEmergencyMode = builder.mIsEmergencyMode;
+ }
+
+ /**
+ * @return Whether satellite is to be enabled
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public boolean isEnabled() {
+ return mIsEnabled;
+ }
+
+ /**
+ * @return Whether demo mode is to be enabled
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public boolean isDemoMode() {
+ return mIsDemoMode;
+ }
+
+ /**
+ * @return Whether satellite is to be enabled for emergency mode
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public boolean isEmergencyMode() {
+ return mIsEmergencyMode;
+ }
+
+ /**
+ * The builder class of {@link EnableRequestAttributes}
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public static final class Builder {
+ private boolean mIsEnabled;
+ private boolean mIsDemoMode = false;
+ private boolean mIsEmergencyMode = false;
+
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public Builder(boolean isEnabled) {
+ mIsEnabled = isEnabled;
+ }
+
+ /**
+ * Set demo mode
+ *
+ * @param isDemoMode {@code true} to enable demo mode and {@code false} to disable. When
+ * disabling satellite, {@code isDemoMode} is always considered as
+ * {@code false} by Telephony.
+ * @return The builder object
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ @NonNull
+ public Builder setDemoMode(boolean isDemoMode) {
+ if (mIsEnabled) {
+ mIsDemoMode = isDemoMode;
+ }
+ return this;
+ }
+
+ /**
+ * Set emergency mode
+ *
+ * @param isEmergencyMode {@code true} means satellite is enabled for emergency mode,
+ * {@code false} otherwise. When disabling satellite,
+ * {@code isEmergencyMode} is always considered as {@code false} by
+ * Telephony.
+ * @return The builder object
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ @NonNull
+ public Builder setEmergencyMode(boolean isEmergencyMode) {
+ if (mIsEnabled) {
+ mIsEmergencyMode = isEmergencyMode;
+ }
+ return this;
+ }
+
+ /**
+ * Build the {@link EnableRequestAttributes}
+ *
+ * @return The {@link EnableRequestAttributes} instance.
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ @NonNull
+ public EnableRequestAttributes build() {
+ return new EnableRequestAttributes(this);
+ }
+ }
+}
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index b97822a..2c141cf 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -162,6 +162,13 @@
/**
* Bundle key to get the response from
+ * {@link #requestIsEmergencyModeEnabled(Executor, OutcomeReceiver)}.
+ * @hide
+ */
+ public static final String KEY_EMERGENCY_MODE_ENABLED = "emergency_mode_enabled";
+
+ /**
+ * Bundle key to get the response from
* {@link #requestIsSupported(Executor, OutcomeReceiver)}.
* @hide
*/
@@ -341,6 +348,13 @@
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23;
+ /**
+ * Telephony framework timeout to receive ACK or response from the satellite modem after
+ * sending a request to the modem.
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24;
+
/** @hide */
@IntDef(prefix = {"SATELLITE_RESULT_"}, value = {
SATELLITE_RESULT_SUCCESS,
@@ -366,7 +380,8 @@
SATELLITE_RESULT_NOT_SUPPORTED,
SATELLITE_RESULT_REQUEST_IN_PROGRESS,
SATELLITE_RESULT_MODEM_BUSY,
- SATELLITE_RESULT_ILLEGAL_STATE
+ SATELLITE_RESULT_ILLEGAL_STATE,
+ SATELLITE_RESULT_MODEM_TIMEOUT
})
@Retention(RetentionPolicy.SOURCE)
public @interface SatelliteResult {}
@@ -482,9 +497,7 @@
* aligned with the satellite, user can send a message and also receive a reply in demo mode.
* If enableSatellite is {@code false}, enableDemoMode has no impact on the behavior.
*
- * @param enableSatellite {@code true} to enable the satellite modem and
- * {@code false} to disable.
- * @param enableDemoMode {@code true} to enable demo mode and {@code false} to disable.
+ * @param attributes The attributes of the enable request.
* @param executor The executor on which the error code listener will be called.
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
@@ -493,9 +506,10 @@
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
- public void requestEnabled(boolean enableSatellite, boolean enableDemoMode,
+ public void requestEnabled(@NonNull EnableRequestAttributes attributes,
@NonNull @CallbackExecutor Executor executor,
@SatelliteResult @NonNull Consumer<Integer> resultListener) {
+ Objects.requireNonNull(attributes);
Objects.requireNonNull(executor);
Objects.requireNonNull(resultListener);
@@ -509,8 +523,8 @@
() -> resultListener.accept(result)));
}
};
- telephony.requestSatelliteEnabled(mSubId, enableSatellite, enableDemoMode,
- errorCallback);
+ telephony.requestSatelliteEnabled(mSubId, attributes.isEnabled(),
+ attributes.isDemoMode(), attributes.isEmergencyMode(), errorCallback);
} else {
throw new IllegalStateException("telephony service is null.");
}
@@ -631,6 +645,61 @@
}
/**
+ * Request to get whether the satellite service is enabled for emergency mode.
+ *
+ * @param executor The executor on which the callback will be called.
+ * @param callback The callback object to which the result will be delivered.
+ * If the request is successful, {@link OutcomeReceiver#onResult(Object)}
+ * will return a {@code boolean} with value {@code true} if satellite is enabled
+ * for emergency mode and {@code false} otherwise.
+ * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)}
+ * will return a {@link SatelliteException} with the {@link SatelliteResult}.
+ *
+ * @throws SecurityException if the caller doesn't have required permission.
+ */
+ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public void requestIsEmergencyModeEnabled(@NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ ResultReceiver receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == SATELLITE_RESULT_SUCCESS) {
+ if (resultData.containsKey(KEY_EMERGENCY_MODE_ENABLED)) {
+ boolean isEmergencyModeEnabled =
+ resultData.getBoolean(KEY_EMERGENCY_MODE_ENABLED);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onResult(isEmergencyModeEnabled)));
+ } else {
+ loge("KEY_EMERGENCY_MODE_ENABLED does not exist.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onError(new SatelliteException(
+ SATELLITE_RESULT_REQUEST_FAILED))));
+ }
+ } else {
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onError(new SatelliteException(resultCode))));
+ }
+ }
+ };
+ telephony.requestIsEmergencyModeEnabled(mSubId, receiver);
+ } else {
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+ }
+ } catch (RemoteException ex) {
+ loge("requestIsEmergencyModeEnabled() RemoteException: " + ex);
+ ex.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
* Request to get whether the satellite service is supported on the device.
*
* <p>
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 3f41d56..cc770aa 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -376,4 +376,12 @@
* @param data with the sim specific configs to be backed up.
*/
void restoreAllSimSpecificSettingsFromBackup(in byte[] data);
+
+ /**
+ * Set the transfer status of the subscriptionInfo that corresponds to subId.
+ * @param subId The unique SubscriptionInfo key in database.
+ * @param status The transfer status to change. This value must be one of the following.
+ */
+ @EnforcePermission("WRITE_EMBEDDED_SUBSCRIPTIONS")
+ void setTransferStatus(int subId, int status);
}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 213fbc5..bd47b1f 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -2742,14 +2742,19 @@
* Request to enable or disable the satellite modem.
*
* @param subId The subId of the subscription to enable or disable the satellite modem for.
- * @param enable True to enable the satellite modem and false to disable.
- * @param isDemoModeEnabled True if demo mode is enabled and false otherwise.
+ * @param enableSatellite True to enable the satellite modem and false to disable.
+ * @param enableDemoMode True if demo mode is enabled and false otherwise. When
+ * disabling satellite, {@code enableDemoMode} is always considered as
+ * {@code false} by Telephony.
+ * @param isEmergency {@code true} means satellite is enabled for emergency mode, {@code false}
+ * otherwise. When disabling satellite, {@code isEmergency} is always
+ * considered as {@code false} by Telephony.
* @param callback The callback to get the result of the request.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void requestSatelliteEnabled(int subId, boolean enable, boolean isDemoModeEnabled,
- in IIntegerConsumer callback);
+ void requestSatelliteEnabled(int subId, boolean enableSatellite, boolean enableDemoMode,
+ boolean isEmergency, in IIntegerConsumer callback);
/**
* Request to get whether the satellite modem is enabled.
@@ -2775,6 +2780,18 @@
void requestIsDemoModeEnabled(int subId, in ResultReceiver receiver);
/**
+ * Request to get whether the satellite service is enabled with emergency mode.
+ *
+ * @param subId The subId of the subscription to request whether the satellite demo mode is
+ * enabled for.
+ * @param receiver Result receiver to get the error code of the request and whether the
+ * satellite is enabled with emergency mode.
+ */
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+ void requestIsEmergencyModeEnabled(int subId, in ResultReceiver receiver);
+
+ /**
* Request to get whether the satellite service is supported on the device.
*
* @param subId The subId of the subscription to check whether satellite is supported for.
diff --git a/telephony/java/com/android/internal/telephony/PhoneConstants.java b/telephony/java/com/android/internal/telephony/PhoneConstants.java
index 07f2916..a9ebd5c 100644
--- a/telephony/java/com/android/internal/telephony/PhoneConstants.java
+++ b/telephony/java/com/android/internal/telephony/PhoneConstants.java
@@ -250,9 +250,6 @@
*/
public static final int DOMAIN_NON_3GPP_PS = 4;
- /** Key to enable comparison of domain selection results from legacy and new code. */
- public static final String EXTRA_COMPARE_DOMAIN = "compare_domain";
-
/** The key to specify the emergency service category */
public static final String EXTRA_EMERGENCY_SERVICE_CATEGORY = "emergency_service_category";
}
diff --git a/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl b/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl
index 19f1a5b..053bc7d 100644
--- a/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl
+++ b/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl
@@ -57,4 +57,7 @@
boolean isSimPortAvailable(int cardId, int portIndex, String callingPackage);
boolean hasCarrierPrivilegesForPackageOnAnyPhone(String callingPackage);
boolean isCompatChangeEnabled(String callingPackage, long changeId);
+ void setPsimConversionSupportedCarriers(in int[] carrierIds);
+ boolean isPsimConversionSupported(in int carrierId);
+ long getAvailableMemoryInBytes(int cardId, String callingPackage);
}
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index 73cc2f2..f628af1 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -340,6 +340,14 @@
wmHelper.StateSyncBuilder().withPipGone().withHomeActivityVisible().waitForAndVerify()
}
+ open fun tapPipToShowMenu(wmHelper: WindowManagerStateHelper) {
+ val windowRect = getWindowRect(wmHelper)
+ uiDevice.click(windowRect.centerX(), windowRect.centerY())
+ // search and interact with the dismiss button
+ val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss")
+ uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT)
+ }
+
/** Close the pip window by pressing the expand button */
fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) {
val windowRect = getWindowRect(wmHelper)